Merge pull request 'nico/19-des-25' (#44) from nico/19-des-25 into staggingweb

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/44
This commit is contained in:
2025-12-19 15:44:53 +08:00
133 changed files with 4310 additions and 1942 deletions

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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../../_state/desa/berita'; import stateDashboardBerita from '../../../_state/desa/berita';
import { useDebouncedValue } from '@mantine/hooks';
function KategoriBerita() { function KategoriBerita() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -48,6 +49,7 @@ function ListKategoriBerita({ 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,
@@ -58,8 +60,8 @@ function ListKategoriBerita({ search }: { search: string }) {
} = listDataState.findMany; } = listDataState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
@@ -81,77 +83,84 @@ function ListKategoriBerita({ search }: { search: string }) {
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
<Title order={4}>Daftar Kategori Berita</Title> <Title order={4} lh={1.2}>
<Button Daftar Kategori Berita
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => color="blue"
router.push('/admin/desa/berita/kategori-berita/create') variant="light"
} onClick={() =>
> router.push('/admin/desa/berita/kategori-berita/create')
Tambah Baru }
</Button> >
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh> <TableTh w="50%">
<TableTh style={{ width: '50%' }}>Nama</TableTh> <Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
<TableTh style={{ width: '20%' }}>Edit</TableTh> </TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh> <TableTh w="20%">
<Text fz="sm" fw={600} lh={1.4} ta="center">Edit</Text>
</TableTh>
<TableTh w="20%">
<Text fz="sm" fw={600} lh={1.4} ta="center">Hapus</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item, index) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fz="sm">{index + 1}</Text> <Text fz="sm" fw={500} lh={1.45} truncate="end">
</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd ta="center">
<Button <Button
variant="light" variant="light"
color="green" color="green"
onClick={() => onClick={() =>
router.push( router.push(
`/admin/desa/berita/kategori-berita/${item.id}` `/admin/desa/berita/kategori-berita/${item.id}`
) )
} }
> size="compact-sm"
<IconEdit size={18} /> >
</Button> <IconEdit size={16} />
</Button>
</TableTd> </TableTd>
<TableTd> <TableTd ta="center">
<Button <Button
variant="light" variant="light"
color="red" color="red"
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {
setSelectedId(item.id); setSelectedId(item.id);
setModalHapus(true); setModalHapus(true);
}} }}
> size="compact-sm"
<IconTrash size={18} /> >
</Button> <IconTrash size={16} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={24}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori berita yang cocok Tidak ada data kategori berita yang cocok
</Text> </Text>
</Center> </Center>
@@ -161,22 +170,70 @@ function ListKategoriBerita({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs" mt="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder radius="md" p="sm" bg="white">
<Box flex={1} ml="md">
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
<Text fz="sm" fw={500} lh={1.45} truncate>
{item.name}
</Text>
</Box>
<Group mt="sm" justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
size="compact-xs"
onClick={() =>
router.push(
`/admin/desa/berita/kategori-berita/${item.id}`
)
}
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
size="compact-xs"
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={14} />
</Button>
</Group>
</Paper>
))
) : (
<Center py={32}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori berita yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> {totalPages > 1 && (
<Pagination <Center mt={{ base: 'lg', md: 'xl' }}>
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10, search); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" color="blue"
color="blue" radius="md"
radius="md" />
/> </Center>
</Center> )}
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
@@ -189,4 +246,4 @@ function ListKategoriBerita({ search }: { search: string }) {
); );
} }
export default KategoriBerita; export default KategoriBerita;

View File

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

View File

@@ -41,7 +41,7 @@ function DetailBerita() {
const data = beritaState.berita.findUnique.data; const data = beritaState.berita.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Back */} {/* Tombol Back */}
<Button <Button
variant="subtle" variant="subtle"

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,16 +45,17 @@ function Berita() {
function ListBerita({ search }: { search: string }) { function ListBerita({ search }: { search: string }) {
const beritaState = useProxy(stateDashboardBerita); const beritaState = useProxy(stateDashboardBerita);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = beritaState.berita.findMany; const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
@@ -63,64 +64,66 @@ function ListBerita({ search }: { search: string }) {
const filteredData = data || []; const filteredData = data || [];
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Berita</Title> <Title order={4}>Daftar Berita</Title>
<Button <Button
leftSection={<IconCircleDashedPlus size={18} />} leftSection={<IconCircleDashedPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/desa/berita/list-berita/create')} onClick={() => router.push('/admin/desa/berita/list-berita/create')}
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh> <TableTh w="50%">Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh> <TableTh w="30%">Kategori</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh w="20%">Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '30%' }}> <TableTd>
<Box w={150}> <Text fz="md" fw={600} lh={1.45} truncate="end">
<Text fw={500} truncate="end" lineClamp={1}> {item.judul}
{item.judul} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed" lh={1.45}>
{item.kategoriBerita?.name || '-'} {item.kategoriBerita?.name || '-'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
onClick={() => onClick={() =>
router.push(`/admin/desa/berita/list-berita/${item.id}`) router.push(`/admin/desa/berita/list-berita/${item.id}`)
} }
fz="sm"
px="sm"
h={36}
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={18} />
<Text ml={5}>Detail</Text> <Text ml="xs">Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data berita yang cocok Tidak ada data berita yang cocok
</Text> </Text>
</Center> </Center>
@@ -130,6 +133,52 @@ function ListBerita({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="sm" mt="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Text fz="sm" fw={600} lh={1.4} c="dimmed">
Judul
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.judul}
</Text>
<Text fz="sm" fw={600} lh={1.4} c="dimmed" mt="xs">
Kategori
</Text>
<Text fz="sm" lh={1.45} fw={500}>
{item.kategoriBerita?.name || '-'}
</Text>
<Button
variant="light"
color="blue"
fullWidth
mt="sm"
onClick={() =>
router.push(`/admin/desa/berita/list-berita/${item.id}`)
}
fz="sm"
h={36}
>
<IconDeviceImacCog size={18} />
<Text ml="xs">Detail</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data berita yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>
@@ -150,4 +199,4 @@ function ListBerita({ search }: { search: string }) {
); );
} }
export default Berita; export default Berita;

View File

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

View File

@@ -45,7 +45,7 @@ function DetailFoto() {
const imageUrl = data.imageGalleryFoto?.link; const imageUrl = data.imageGalleryFoto?.link;
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()}
@@ -59,7 +59,7 @@ function DetailFoto() {
withBorder withBorder
// Gunakan max-width agar tidak terlalu lebar di desktop // Gunakan max-width agar tidak terlalu lebar di desktop
maw={800} maw={800}
w="100%" w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,6 +45,7 @@ function Foto() {
function ListFoto({ search }: { search: string }) { function ListFoto({ search }: { search: string }) {
const FotoState = useProxy(stateGallery.foto) const FotoState = useProxy(stateGallery.foto)
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -55,76 +56,81 @@ function ListFoto({ search }: { search: string }) {
} = FotoState.findMany; } = FotoState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <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 Foto</Title> <Title order={4} lh={1.2}>Daftar Foto</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/desa/gallery/foto/create')} onClick={() => router.push('/admin/desa/gallery/foto/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: '25%' }}>Judul Foto</TableTh> <TableTh>Judul Foto</TableTh>
<TableTh style={{ width: '20%' }}>Tanggal</TableTh> <TableTh>Tanggal</TableTh>
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh>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 style={{ width: '25%' }}> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text> {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Box w={200}> <Text fz="sm" c="dimmed" lh={1.45}>
<Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '30%' }}> <TableTd>
<Box w={200}> <Text
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> fz="sm"
</Box> lh={1.45}
truncate="end"
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
size="xs"
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)} onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={16} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -133,7 +139,7 @@ function ListFoto({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text c="dimmed">Tidak ada foto yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>Tidak ada foto yang cocok</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -141,7 +147,54 @@ function ListFoto({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder radius="sm" p="md">
<Stack gap="xs">
<Box>
<Text fz="sm" fw={600} lh={1.4}>Judul Foto</Text>
<Text fz="sm" fw={500} lh={1.45}>{item.name}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Tanggal</Text>
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" fw={500} lh={1.45} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
<Button
variant="light"
color="blue"
size="xs"
fullWidth
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
>
<IconDeviceImac size={16} />
<Text ml={5} fz="sm" fw={500}>Detail</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>Tidak ada foto yang cocok</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -160,4 +213,4 @@ function ListFoto({ search }: { search: string }) {
); );
} }
export default Foto; export default Foto;

View File

@@ -118,7 +118,7 @@ function EditVideo() {
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,6 +45,7 @@ function Video() {
function ListVideo({ search }: { search: string }) { function ListVideo({ search }: { search: string }) {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video)
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -55,75 +56,77 @@ function ListVideo({ search }: { search: string }) {
} = videoState.findMany; } = videoState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={20}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={20}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Video</Title> <Title order={4} lh={1.2}>
<Button Daftar Video
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/desa/gallery/video/create')} color="blue"
> variant="light"
Tambah Baru onClick={() => router.push('/admin/desa/gallery/video/create')}
</Button> >
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover> {/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover w="100%">
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Judul Video</TableTh> <TableTh>Judul Video</TableTh>
<TableTh style={{ width: '20%' }}>Tanggal</TableTh> <TableTh>Tanggal</TableTh>
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh>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 style={{ width: '25%' }}> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text> {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Box w={200}> <Text fz="sm" c="dimmed" lh={1.45}>
<Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '30%' }}> <TableTd>
<Box w={200}> <Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)} onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
fz="sm"
px="xs"
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={18} />
<Text ml={5}>Detail</Text> <Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
@@ -132,8 +135,10 @@ function ListVideo({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={24}>
<Text c="dimmed">Tidak ada video yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -141,23 +146,74 @@ function ListVideo({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs" mt="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} p="sm" withBorder radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Judul Video</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Tanggal</Text>
<Text fz="sm" fw={500} lh={1.45}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" lineClamp={5} fw={500} lh={1.45} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
<Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
fz="sm"
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center>
<Pagination {totalPages > 1 && (
value={page} <Center mt="xl">
onChange={(newPage) => { <Pagination
load(newPage, 10) value={page}
window.scrollTo({ top: 0, behavior: 'smooth' }) onChange={(newPage) => {
}} load(newPage, 10)
total={totalPages} window.scrollTo({ top: 0, behavior: 'smooth' })
mt="md" }}
mb="md" total={totalPages}
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
)}
</Box> </Box>
); );
} }
export default Video; export default Video;

View File

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

View File

@@ -48,7 +48,7 @@ function DetailAjukanPermohonan() {
const data = ajukanPermohonanState.findUnique.data; const data = ajukanPermohonanState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -61,7 +61,7 @@ function DetailAjukanPermohonan() {
<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

@@ -24,6 +24,7 @@ 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 stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useDebouncedValue } from '@mantine/hooks';
function AjukanPermohonan() { function AjukanPermohonan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -44,6 +45,7 @@ function AjukanPermohonan() {
function ListAjukanPermohonan({ search }: { search: string }) { function ListAjukanPermohonan({ search }: { search: string }) {
const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan); const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
@@ -54,58 +56,56 @@ function ListAjukanPermohonan({ search }: { search: string }) {
} = AjukanPermohonanState.findMany; } = AjukanPermohonanState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
// Loading state // Loading state
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">
<Title order={4}>List Ajukan Permohonan</Title> <Title order={2} lh={1.2} mb={{ base: 'md', md: 'lg' }}>
<Box style={{ overflowX: "auto" }}> List Ajukan Permohonan
<Table highlightOnHover> </Title>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Alamat</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Alamat</TableTh>
<TableTh style={{ width: '15%' }}>NIK</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>NIK</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{data.length > 0 ? ( {data.length > 0 ? (
data.map((item) => ( data.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '30%' }}> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.nama}
{item.nama} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '45%' }}> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.alamat}
{item.alamat} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '45%' }}> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.nik}
{item.nik} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
size="xs" size="xs"
radius="md" radius="md"
@@ -123,9 +123,11 @@ function ListAjukanPermohonan({ search }: { search: string }) {
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text color="dimmed">Tidak ada data ajukan permohonan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data ajukan permohonan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -133,23 +135,71 @@ function ListAjukanPermohonan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="md">
{data.length > 0 ? (
data.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md" shadow="xs">
<Stack gap={'xs'}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.nama}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Alamat</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.alamat}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>NIK</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.nik}</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
}
fullWidth
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data ajukan permohonan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center>
<Pagination {totalPages > 1 && (
value={page} <Center mt="md">
onChange={(newPage) => { <Pagination
load(newPage, 10, search); value={page}
window.scrollTo({ top: 0, behavior: 'smooth' }); onChange={(newPage) => {
}} load(newPage, 10, search);
total={totalPages} window.scrollTo({ top: 0, behavior: 'smooth' });
mt="md" }}
mb="md" total={totalPages}
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
)}
</Box> </Box>
); );
} }
export default AjukanPermohonan; export default AjukanPermohonan;

View File

@@ -108,7 +108,7 @@ function EditPelayananPendudukNonPermanent() {
}; };
return ( return (
<Box> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Stack gap="xs"> <Stack gap="xs">
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -45,24 +45,28 @@ function PelayananPendudukNonPermanent() {
{/* Header */} {/* Header */}
<Grid align="center"> <Grid align="center">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}> <Title
order={3}
lh={1.2}
c={colors['blue-button']}
>
Preview Pelayanan Penduduk Non Permanen Preview Pelayanan Penduduk Non Permanen
</Title> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
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={() => onClick={() =>
router.push( router.push(
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}` `/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
) )
} }
> >
Edit Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
@@ -70,14 +74,14 @@ function PelayananPendudukNonPermanent() {
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 0, md: 50 }} pb="xl"> <Box px={{ base: 0, md: 50 }} pb="xl">
<Center> <Center>
<Text <Title
order={2}
lh={1.2}
ta="center" ta="center"
fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']} c={colors['blue-button']}
> >
{data.name} {data.name}
</Text> </Title>
</Center> </Center>
<Divider my="md" color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
@@ -86,9 +90,11 @@ function PelayananPendudukNonPermanent() {
<Text <Text
py={10} py={10}
ta="justify" ta="justify"
fz={{ base: '1rem', md: '1.2rem' }} fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.55 }}
c="dark"
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</Box> </Box>
</Box> </Box>
@@ -98,4 +104,4 @@ function PelayananPendudukNonPermanent() {
); );
} }
export default PelayananPendudukNonPermanent; export default PelayananPendudukNonPermanent;

View File

@@ -123,7 +123,7 @@ function EditPelayananPerizinanBerusaha() {
} }
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="xs"> <Stack gap="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">

View File

@@ -41,8 +41,7 @@ function PerizinanBerusaha() {
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
// You should get the ID from your router query or params const id = 'edit';
const id = 'edit'; // Replace with actual ID or get from URL params
await pelayananPerizinanBerusaha.findById.load(id); await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) { } catch (err) {
setError('Gagal memuat data'); setError('Gagal memuat data');
@@ -66,7 +65,7 @@ function PerizinanBerusaha() {
if (error || !pelayananPerizinanBerusaha.findById.data) { if (error || !pelayananPerizinanBerusaha.findById.data) {
return ( return (
<Center h={200}> <Center h={200}>
<Text>{error || 'Data tidak ditemukan'}</Text> <Text c="dimmed">{error || 'Data tidak ditemukan'}</Text>
</Center> </Center>
); );
} }
@@ -79,24 +78,24 @@ function PerizinanBerusaha() {
{/* Header */} {/* Header */}
<Grid align="center"> <Grid align="center">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}> <Title order={3} c={colors['blue-button']} lh={1.2}>
Preview Pelayanan Perizinan Berusaha Preview Pelayanan Perizinan Berusaha
</Title> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
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={() => onClick={() =>
router.push( router.push(
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}` `/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
) )
} }
> >
Edit Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
@@ -104,38 +103,40 @@ function PerizinanBerusaha() {
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 0, md: 50 }} pb="xl"> <Box px={{ base: 0, md: 50 }} pb="xl">
<Center> <Center>
<Text <Title
order={3}
ta="center" ta="center"
fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']} c={colors['blue-button']}
lh={1.15}
> >
{data.name} {data.name}
</Text> </Title>
</Center> </Center>
<Divider my="md" color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
<Box mt="lg"> <Box mt="lg">
<Text <Text
py={10} py="xs"
ta="justify" ta={{ base: "left", md: "justify" }}
fz={{ base: '1rem', md: '1.2rem' }} fz={{ base: 'sm', md: 'md' }}
lh={1.55}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />
<Text <Text
py={10} py="xs"
fz={{ base: '1rem', md: '1.2rem' }} fz={{ base: 'sm', md: 'md' }}
fw="bold" fw={700}
c={colors['blue-button']} c={colors['blue-button']}
lh={1.5}
> >
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
umum: umum:
</Text> </Text>
<Box p="xl" w="100%"> <Box p="xl" w="100%" visibleFrom='md'>
<Stepper <Stepper
active={active} active={active}
onStepClick={setActive} onStepClick={setActive}
@@ -143,28 +144,115 @@ function PerizinanBerusaha() {
styles={{ styles={{
separator: { marginLeft: 25 }, separator: { marginLeft: 25 },
step: { padding: '12px 0' }, step: { padding: '12px 0' },
stepLabel: {
fontSize: 'var(--mantine-font-size-sm)',
fontWeight: 700,
lineHeight: 1.4,
},
stepDescription: {
fontSize: 'var(--mantine-font-size-xs)',
lineHeight: 1.4,
},
}} }}
> >
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun"> <StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
Pendaftaran akun pada portal OSS <Text fz="sm" lh={1.5}>
Pendaftaran akun pada portal OSS
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan"> <StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya <Text fz="sm" lh={1.5}>
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI"> <StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
Memilih KBLI dengan jenis usaha yang akan didaftarkan <Text fz="sm" lh={1.5}>
Memilih KBLI dengan jenis usaha yang akan didaftarkan
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen"> <StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku <Text fz="sm" lh={1.5}>
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan"> <StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
Proses verifikasi dan persetujuan oleh instansi terkait <Text fz="sm" lh={1.5}>
Proses verifikasi dan persetujuan oleh instansi terkait
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Keenam" description="Penerimaan NIB"> <StepperStep label="Langkah Keenam" description="Penerimaan NIB">
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda <Text fz="sm" lh={1.5}>
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
</Text>
</StepperStep> </StepperStep>
<StepperCompleted> <StepperCompleted>
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS <Text fz="sm" lh={1.5}>
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
</Text>
</StepperCompleted>
</Stepper>
<Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>
Back
</Button>
<Button onClick={nextStep}>Next step</Button>
</Group>
</Box>
<Box p="xl" w="100%" hiddenFrom='md'>
<Stepper
active={active}
onStepClick={setActive}
orientation="vertical"
styles={{
separator: { marginLeft: 25 },
step: { padding: '12px 0' },
stepLabel: {
fontSize: 'var(--mantine-font-size-sm)',
fontWeight: 700,
lineHeight: 1.4,
},
stepDescription: {
fontSize: 'var(--mantine-font-size-xs)',
lineHeight: 1.4,
},
}}
>
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
<Text fz="sm" lh={1.5}>
</Text>
</StepperStep>
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
<Text fz="sm" lh={1.5}>
</Text>
</StepperStep>
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
<Text fz="sm" lh={1.5}>
</Text>
</StepperStep>
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
<Text fz="sm" lh={1.5}>
</Text>
</StepperStep>
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
<Text fz="sm" lh={1.5}>
</Text>
</StepperStep>
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
<Text fz="sm" lh={1.5}>
</Text>
</StepperStep>
<StepperCompleted>
<Text fz="sm" lh={1.5}>
</Text>
</StepperCompleted> </StepperCompleted>
</Stepper> </Stepper>
@@ -177,9 +265,10 @@ function PerizinanBerusaha() {
</Box> </Box>
<Text <Text
py={35} py="md"
ta="justify" ta={{ base: "left", md: "justify" }}
fz={{ base: '1rem', md: '1.2rem' }} fz={{ base: 'sm', md: 'md' }}
lh={1.55}
> >
Penting untuk diingat bahwa prosedur dan persyaratan dapat Penting untuk diingat bahwa prosedur dan persyaratan dapat
berubah seiring waktu. Untuk informasi yang lebih akurat dan berubah seiring waktu. Untuk informasi yang lebih akurat dan
@@ -203,5 +292,4 @@ function PerizinanBerusaha() {
); );
} }
export default PerizinanBerusaha; export default PerizinanBerusaha;

View File

@@ -64,7 +64,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({
}; };
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
{title} {title}
</Text> </Text>

View File

@@ -49,7 +49,7 @@ function DetailSuratKeterangan() {
const data = suratKeteranganState.findUnique.data; const data = suratKeteranganState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -62,7 +62,7 @@ function DetailSuratKeterangan() {
<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"
@@ -75,20 +75,21 @@ function DetailSuratKeterangan() {
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box> <Stack gap={"xs"}>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
Nama Nama
</Text> </Text>
<Text fz="md" c="dimmed"> <Text fz="md" c="dimmed">
{data?.name || '-'} {data?.name || '-'}
</Text> </Text>
</Box> </Stack>
<Box> <Stack gap={"xs"}>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
Deskripsi Deskripsi
</Text> </Text>
<Text <Box pl={10}>
<Text
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@@ -96,9 +97,10 @@ function DetailSuratKeterangan() {
}} }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
</Stack>
<Box> <Stack gap={"xs"}>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
Gambar Konten Pelayanan Gambar Konten Pelayanan
</Text> </Text>
@@ -117,7 +119,7 @@ function DetailSuratKeterangan() {
Tidak ada gambar Tidak ada gambar
</Text> </Text>
)} )}
</Box> </Stack>
<Box> <Box>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">

View File

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

View File

@@ -17,7 +17,7 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
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';
@@ -25,9 +25,10 @@ import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useDebouncedValue } from '@mantine/hooks';
function SuratKeterangan() { function SuratKeterangan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -45,6 +46,7 @@ function SuratKeterangan() {
function ListSuratKeterangan({ search }: { search: string }) { function ListSuratKeterangan({ search }: { search: string }) {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan); const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -55,72 +57,80 @@ function ListSuratKeterangan({ search }: { search: string }) {
} = suratKeteranganState.findMany; } = suratKeteranganState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!data) return []; if (!data) return [];
const keyword = search.toLowerCase(); const keyword = debouncedSearch.toLowerCase();
return data.filter(item => return data.filter(
item.name?.toLowerCase().includes(keyword) || (item) =>
item.deskripsi?.toLowerCase().includes(keyword) item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
); );
}, [data, search]); }, [data, debouncedSearch]);
// Loading state
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: '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 Surat Keterangan</Title> <Title order={4} lh={1.2}>
<Button List Surat Keterangan
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => color="blue"
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create') variant="light"
} onClick={() =>
> router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
Tambah Baru }
</Button> >
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh> <TableTh fz="sm" fw={600} ta="left">
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh> Nama
<TableTh style={{ width: '15%' }}>Aksi</TableTh> </TableTh>
<TableTh fz="sm" fw={600} ta="left">
Deskripsi
</TableTh>
<TableTh fz="sm" fw={600} ta="left">
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 style={{ width: '30%' }}> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} truncate="end">
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '45%' }}> <TableTd>
<Box w={200}> <Text
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed" fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }} lh={1.5}
style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.deskripsi || '' }}
/> style={{ wordBreak: 'break-word' }}
</Box> />
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
size="xs" size="xs"
radius="md" radius="md"
@@ -128,7 +138,9 @@ function ListSuratKeterangan({ search }: { search: string }) {
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`) router.push(
`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`
)
} }
> >
Detail Detail
@@ -139,8 +151,10 @@ function ListSuratKeterangan({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text> <Text c="dimmed" fz="sm" ta="center">
Tidak ada data surat keterangan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -148,7 +162,67 @@ function ListSuratKeterangan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={'xs'}>
<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}>
Deskripsi
</Text>
<Box pl={8}>
<Text
fz="sm"
fw={500}
lh={1.4}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '' }}
style={{ wordBreak: 'break-word' }}
/>
</Box>
</Box>
<Box>
<Button
fullWidth
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`
)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" ta="center">
Tidak ada data surat keterangan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -167,4 +241,4 @@ function ListSuratKeterangan({ search }: { search: string }) {
); );
} }
export default SuratKeterangan; export default SuratKeterangan;

View File

@@ -74,13 +74,13 @@ function EditPelayananTelunjukSakti() {
); );
const handleResetForm = () => { const handleResetForm = () => {
setFormData({ setFormData({
name: originalData.name, name: originalData.name,
deskripsi: originalData.deskripsi, deskripsi: originalData.deskripsi,
link: originalData.link, link: originalData.link,
}); });
toast.info("Form dikembalikan ke data awal"); toast.info("Form dikembalikan ke data awal");
}; };
// Submit: update global state hanya saat simpan // Submit: update global state hanya saat simpan
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -102,12 +102,12 @@ function EditPelayananTelunjukSakti() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Back Button + Title */} {/* Back Button + Title */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<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 Pelayanan Telunjuk Sakti Desa Edit Pelayanan Telunjuk Sakti Desa
</Title> </Title>

View File

@@ -50,7 +50,7 @@ function DetailPelayananTelunjukSakti() {
const data = telunjukSaktiState.findUnique.data; const data = telunjukSaktiState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -63,7 +63,7 @@ function DetailPelayananTelunjukSakti() {
<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

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

View File

@@ -1,5 +1,5 @@
/* 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 { import {
@@ -18,7 +18,7 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
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';
@@ -26,9 +26,10 @@ 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 stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useDebouncedValue } from '@mantine/hooks';
function PelayananTelunjukSakti() { function PelayananTelunjukSakti() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -46,46 +47,57 @@ function PelayananTelunjukSakti() {
function ListPelayananTelunjukSakti({ search }: { search: string }) { function ListPelayananTelunjukSakti({ search }: { search: string }) {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany; const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={400} radius="md" /> <Skeleton height={400} 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="md">
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title> <Title order={4} lh={1.2}>
<Button Daftar Pelayanan Telunjuk Sakti
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => color="blue"
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create') variant="light"
} onClick={() =>
> router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
Tambah Baru }
</Button> >
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover style={{ minWidth: '700px' }}> {/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh> <TableTh fz="sm" fw={600} ta="left" c="gray.8" w="30%">
<TableTh style={{ width: '40%' }}>Link</TableTh> Nama
<TableTh style={{ width: '30%' }}>Detail</TableTh> </TableTh>
<TableTh fz="sm" fw={600} ta="left" c="gray.8" w="40%">
Link
</TableTh>
<TableTh fz="sm" fw={600} ta="left" c="gray.8" w="30%">
Detail
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -93,18 +105,19 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" fw={500} lh={1.5} truncate="end">
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text></Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <a href={item.link} target="_blank" rel="noopener noreferrer">
<a href={item.link} target="_blank" rel="noopener noreferrer"> <Text
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} style={{wordBreak: "break-word", whiteSpace: "normal"}} truncate="end" fz={"sm"} /> fz="sm"
</a> lh={1.5}
</Box> truncate="end"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</a>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -117,7 +130,9 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
} }
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml="xs" fz="sm" fw={500}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -125,8 +140,8 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py="lg">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data layanan yang cocok Tidak ada data layanan yang cocok
</Text> </Text>
</Center> </Center>
@@ -136,17 +151,68 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
<Stack gap="md">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Box mb="xs">
<Text fz='sm' fw={600} lh={1.4} c="gray.8">
Nama
</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.name}
</Text>
</Box>
<Box mb="xs">
<Text fz='sm' fw={600} lh={1.4} c="gray.8">
Link
</Text>
<Text fz="sm" fw={500} lh={1.5} component="a" href={item.link} target="_blank" rel="noopener noreferrer">
{item.deskripsi}
</Text>
</Box>
<Button
variant="light"
color="blue"
fullWidth
mt="sm"
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml="xs" fz="sm" fw={500}>
Detail
</Text>
</Button>
</Paper>
))}
</Stack>
) : (
<Center py="lg">
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data layanan yang cocok
</Text>
</Center>
)}
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10, search); load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }
}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
@@ -155,5 +221,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
); );
} }
export default PelayananTelunjukSakti; export default PelayananTelunjukSakti;

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ 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 { useDebouncedValue } from '@mantine/hooks';
function Penghargaan() { function Penghargaan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -45,45 +46,48 @@ function Penghargaan() {
function ListPenghargaan({ search }: { search: string }) { function ListPenghargaan({ search }: { search: string }) {
const state = useProxy(penghargaanState); const state = useProxy(penghargaanState);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => { useEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || [];
// Loading state // Loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton h={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="lg">
<Title order={4}>List Penghargaan</Title> <Title order={4}>List Penghargaan</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/desa/penghargaan/create')} onClick={() => router.push('/admin/desa/penghargaan/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: '35%' }}>Nama</TableTh> <TableTh w="35%">Nama</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh> <TableTh w="35%">Deskripsi</TableTh>
<TableTh style={{ width: '30%' }}>Aksi</TableTh> <TableTh w="30%">Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -91,31 +95,27 @@ function ListPenghargaan({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text
<Text fz="sm"
truncate="end" lh={1.45}
lineClamp={1} c="dimmed"
fz="sm" dangerouslySetInnerHTML={{ __html: item.deskripsi }}
c="dimmed" style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} lineClamp={1}
style={{wordBreak: "break-word", whiteSpace: "normal"}} />
/>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<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={() => onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`) router.push(`/admin/desa/penghargaan/${item.id}`)
} }
@@ -127,9 +127,9 @@ function ListPenghargaan({ search }: { search: string }) {
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data penghargaan yang cocok Tidak ada data penghargaan yang cocok
</Text> </Text>
</Center> </Center>
@@ -139,7 +139,54 @@ function ListPenghargaan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Box>
<Text fz="xs" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.name}
</Text>
</Box>
<Box mt="xs">
<Text fz="xs" fw={600} lh={1.4}>
Deskripsi
</Text>
<Text lineClamp={3} fz="sm" fw={500} lh={1.45} c="dimmed">
<span dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Text>
</Box>
<Group mt="md">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`)
}
>
Detail
</Button>
</Group>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data penghargaan yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -148,7 +195,7 @@ function ListPenghargaan({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="lg"
mb="md" mb="md"
color="blue" color="blue"
radius="md" radius="md"
@@ -158,4 +205,4 @@ function ListPenghargaan({ search }: { search: string }) {
); );
} }
export default Penghargaan; export default Penghargaan;

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 { IconCategory, IconListDetails } from '@tabler/icons-react'; import { IconCategory, IconListDetails } 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';
@@ -54,35 +54,76 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
style={{ p="sm"
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", style={{
borderRadius: "1rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", borderRadius: "1rem",
display: "flex", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
flexWrap: "nowrap", display: "flex",
gap: "0.5rem", flexWrap: "nowrap",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi gap: "0.5rem",
}} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
> >
{tabs.map((tab, i) => (
<TabsTab <TabsList
key={i} p="xs" // lebih kecil
value={tab.value} style={{
leftSection={tab.icon} background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
style={{ borderRadius: "1rem",
fontWeight: 600, display: "flex",
fontSize: "0.9rem", flexWrap: "nowrap",
transition: "all 0.2s ease", gap: "0.5rem",
}} width: "max-content", // ⬅️ kunci
> maxWidth: "100%", // ⬅️ penting
{tab.label} }}
</TabsTab> >
))} {tabs.map((tab, i) => (
</TabsList> <TabsTab
</ScrollArea> key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

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

View File

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

View File

@@ -2,11 +2,21 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Button, Center, Box,
Button,
Center,
Pagination, Pagination,
Paper, Skeleton, Stack, Paper,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Skeleton,
Text, Title Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core'; } from '@mantine/core';
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';
@@ -15,6 +25,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '../../../_state/desa/pengumuman'; import stateDesaPengumuman from '../../../_state/desa/pengumuman';
import { useDebouncedValue } from '@mantine/hooks';
function KategoriPengumuman() { function KategoriPengumuman() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -33,90 +44,134 @@ function KategoriPengumuman() {
} }
function ListKategoriPengumuman({ search }: { search: string }) { function ListKategoriPengumuman({ search }: { search: string }) {
const listDataState = useProxy(stateDesaPengumuman.category) const listDataState = useProxy(stateDesaPengumuman.category);
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, 500);
const { data, page, totalPages, loading, load } = listDataState.findMany; const { data, page, totalPages, loading, load } = listDataState.findMany;
useEffect(() => { useEffect(() => {
load(1, 10, search) load(page, 10, debouncedSearch);
}, [search]) }, [page, debouncedSearch]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
load(page, 10, search) load(page, 10, search);
} }
} };
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Stack> <Stack gap={'lg'}>
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}> <Box
<Title order={4}>List Kategori Pengumuman</Title> visibleFrom="md"
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<Title order={4} lh={1.1}>
List Kategori Pengumuman
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')} onClick={() =>
router.push('/admin/desa/pengumuman/kategori-pengumuman/create')
}
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Box> </Box>
<Box style={{ overflowX: 'auto' }}> <Box hiddenFrom="md">
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}> <Stack gap="xs">
<Title order={2} size="md" lh={1.1} ta="left">
List Kategori Pengumuman
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/desa/pengumuman/kategori-pengumuman/create')
}
fullWidth
>
Tambah Baru
</Button>
</Stack>
</Box>
<Box visibleFrom="md">
<Table highlightOnHover striped withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh> <TableTh w="60%">
<TableTh style={{ width: '60%' }}>Nama</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '15%' }}>Edit</TableTh> Nama
<TableTh style={{ width: '15%' }}>Hapus</TableTh> </Text>
</TableTh>
<TableTh w="15%">
<Text fz="sm" fw={600} lh={1.4}>
Edit
</Text>
</TableTh>
<TableTh w="15%">
<Text fz="sm" fw={600} lh={1.4}>
Hapus
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item, index) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text> <Text fz="md" fw={500} lh={1.5} truncate>
</TableTd> {item.name}
<TableTd> </Text>
<Text truncate lineClamp={1}>{item.name}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant='light' variant="light"
color='green' color="green"
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)} onClick={() =>
router.push(
`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`
)
}
size="compact-sm"
> >
<IconEdit size={20} /> <IconEdit size={16} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant='light' variant="light"
color='red' color="red"
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {
setSelectedId(item.id) setSelectedId(item.id);
setModalHapus(true) setModalHapus(true);
}}> }}
<IconTrash size={20} /> size="compact-sm"
>
<IconTrash size={16} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -124,8 +179,10 @@ function ListKategoriPengumuman({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={24}>
<Text c="dimmed">Tidak ada data kategori pengumuman yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori pengumuman yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -133,6 +190,71 @@ function ListKategoriPengumuman({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
<Stack hiddenFrom="md" gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper
key={item.id}
withBorder
p="md"
radius="md"
bg={colors['white-1']}
>
<Stack gap="xs">
<Box>
<Text fz="xs" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box
style={{
display: 'flex',
gap: 8,
justifyContent: 'flex-end',
}}
>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`
)
}
size="compact-xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
size="compact-xs"
>
<IconTrash size={14} />
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Paper withBorder p="xl" radius="md" bg={colors['white-1']}>
<Center>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori pengumuman yang cocok
</Text>
</Center>
</Paper>
)}
</Stack>
</Stack> </Stack>
</Paper> </Paper>
@@ -153,7 +275,7 @@ function ListKategoriPengumuman({ search }: { search: string }) {
text='Apakah anda yakin ingin menghapus kategori Pengumuman ini?' text='Apakah anda yakin ingin menghapus kategori Pengumuman ini?'
/> />
</Box> </Box>
) );
} }
export default KategoriPengumuman; export default KategoriPengumuman;

View File

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

View File

@@ -49,7 +49,7 @@ export default function DetailPengumuman() {
const data = pengumumanState.pengumuman.findUnique.data; const data = pengumumanState.pengumuman.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -61,7 +61,7 @@ export default function DetailPengumuman() {
<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"
@@ -74,14 +74,6 @@ export default function DetailPengumuman() {
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.CategoryPengumuman?.name || '-'}
</Text>
</Box>
<Box> <Box>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
@@ -92,6 +84,15 @@ export default function DetailPengumuman() {
</Text> </Text>
</Box> </Box>
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.CategoryPengumuman?.name || '-'}
</Text>
</Box>
<Box> <Box>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
Deskripsi Deskripsi

View File

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

View File

@@ -19,7 +19,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -46,44 +46,56 @@ function Pengumuman() {
function ListPengumuman({ search }: { search: string }) { function ListPengumuman({ search }: { search: string }) {
const pengumumanState = useProxy(stateDesaPengumuman); const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany; const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ 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 Pengumuman</Title> <Title order={4} lh={1.2}>
Daftar Pengumuman
</Title>
<Button <Button
leftSection={<IconCircleDashedPlus size={18} />} leftSection={<IconCircleDashedPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')} onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
fz={{ base: 'sm', md: 'md' }}
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover style={{ minWidth: '700px' }}> {/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '40%' }}>Judul</TableTh> <TableTh fz="sm" fw={600} ta="left">
<TableTh style={{ width: '30%' }}>Kategori</TableTh> Judul
<TableTh style={{ width: '20%' }}>Detail</TableTh> </TableTh>
<TableTh fz="sm" fw={600} ta="left">
Kategori
</TableTh>
<TableTh fz="sm" fw={600} ta="left">
Detail
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -91,14 +103,12 @@ function ListPengumuman({ search }: { search: string }) {
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.45} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.judul}
{item.judul} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" c="dimmed"> <Text fz="sm" fw={500} lh={1.45} c="dimmed">
{item.CategoryPengumuman?.name || '-'} {item.CategoryPengumuman?.name || '-'}
</Text> </Text>
</TableTd> </TableTd>
@@ -109,9 +119,12 @@ function ListPengumuman({ search }: { search: string }) {
onClick={() => onClick={() =>
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`) router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
} }
fz="sm"
px="sm"
py="xs"
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={18} />
<Text ml={5}>Detail</Text> <Text ml="xs">Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -119,8 +132,10 @@ function ListPengumuman({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py={{ base: 'sm', md: 'md' }}>
<Text color="dimmed">Tidak ada pengumuman yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada pengumuman yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -128,7 +143,59 @@ function ListPengumuman({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card List */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap="xs">
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Judul
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.judul}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Kategori
</Text>
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
{item.CategoryPengumuman?.name || '-'}
</Text>
</Box>
<Box>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
}
fullWidth
fz="sm"
mt="xs"
>
<IconDeviceImacCog size={18} />
<Text ml="xs">Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="sm">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada pengumuman yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -147,4 +214,4 @@ function ListPengumuman({ search }: { search: string }) {
); );
} }
export default Pengumuman; export default Pengumuman;

View File

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

View File

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

View File

@@ -1,7 +1,24 @@
/* 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 { 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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -9,6 +26,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
import { useDebouncedValue } from '@mantine/hooks';
function KategoriPotensi() { function KategoriPotensi() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -27,85 +45,110 @@ function KategoriPotensi() {
} }
function ListKategoriPotensi({ search }: { search: string }) { function ListKategoriPotensi({ search }: { search: string }) {
const listDataState = useProxy(potensiDesaState.kategoriPotensi) const listDataState = useProxy(potensiDesaState.kategoriPotensi);
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 { data, page, totalPages, loading, load } = listDataState.findMany; const { data, page, totalPages, loading, load } = listDataState.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000);
useEffect(() => { useEffect(() => {
load(1, 10, search) load(1, 10, debouncedSearch);
}, [search]) }, [debouncedSearch]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
load(page, 10, search) load(page, 10, search);
} }
} };
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Stack gap="xl">
<Group justify="space-between"> <Group justify="space-between" align="center">
<Title order={4}>List Kategori Potensi</Title> <Title order={4} lh={1.2}>
<Button List Kategori Potensi
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')} color="blue"
> variant="light"
Tambah Baru onClick={() =>
</Button> router.push('/admin/desa/potensi/kategori-potensi/create')
}
>
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}> <Box visibleFrom="md">
<Table highlightOnHover striped withRowBorders miw={700}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh> <TableTh w="60%">
<TableTh style={{ width: '60%' }}>Nama</TableTh> <Text fz="xs" fw={600} lh={1.4} c="black">
<TableTh style={{ width: '15%' }}>Edit</TableTh> Nama
<TableTh style={{ width: '15%' }}>Hapus</TableTh> </Text>
</TableTh>
<TableTh w="15%">
<Text fz="xs" fw={600} lh={1.4} c="black">
Edit
</Text>
</TableTh>
<TableTh w="15%">
<Text fz="xs" fw={600} lh={1.4} c="black">
Hapus
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item, index) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text> <Text fz="sm" lh={1.5} truncate>
{item.nama}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate lineClamp={1}>{item.nama}</Text> <Button
</TableTd> variant="light"
<TableTd> color="green"
<Button variant='light' color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}> onClick={() =>
router.push(
`/admin/desa/potensi/kategori-potensi/${item.id}`
)
}
>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant='light' variant="light"
color='red' color="red"
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {
setSelectedId(item.id) setSelectedId(item.id);
setModalHapus(true) setModalHapus(true);
}}> }}
>
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</TableTd> </TableTd>
@@ -114,8 +157,10 @@ function ListKategoriPotensi({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="lg">
<Text color="dimmed">Tidak ada data kategori potensi yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data kategori potensi yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -123,10 +168,70 @@ function ListKategoriPotensi({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={'xs'}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" lh={1.5}>
{(page - 1) * 10 + index + 1}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" lh={1.5}>
{item.nama}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(
`/admin/desa/potensi/kategori-potensi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="lg">
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data kategori potensi yang cocok
</Text>
</Center>
)}
</Stack>
</Stack> </Stack>
</Paper> </Paper>
<Center mt="md"> <Center mt="xl">
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage, 10, search)} onChange={(newPage) => load(newPage, 10, search)}
@@ -143,7 +248,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
text='Apakah anda yakin ingin menghapus kategori Potensi ini?' text='Apakah anda yakin ingin menghapus kategori Potensi ini?'
/> />
</Box> </Box>
) );
} }
export default KategoriPotensi; export default KategoriPotensi;

View File

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

View File

@@ -40,7 +40,7 @@ export default function DetailPotensi() {
const data = potensiState.findUnique.data; const data = potensiState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -52,7 +52,7 @@ export default function DetailPotensi() {
<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

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

View File

@@ -26,6 +26,7 @@ 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 potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
import { useDebouncedValue } from '@mantine/hooks';
function Potensi() { function Potensi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -46,6 +47,7 @@ function Potensi() {
function ListPotensi({ search }: { search: string }) { function ListPotensi({ search }: { search: string }) {
const potensiState = useProxy(potensiDesaState); const potensiState = useProxy(potensiDesaState);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -57,41 +59,61 @@ function ListPotensi({ search }: { search: string }) {
useEffect(() => { useEffect(() => {
potensiState.kategoriPotensi.findMany.load(); potensiState.kategoriPotensi.findMany.load();
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ 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 Potensi Desa</Title> <Title order={4} lh={1.2}>
<Button Daftar Potensi Desa
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')} color="blue"
> variant="light"
Tambah Baru onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
</Button> >
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover style={{ minWidth: '700px' }}> {/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={700}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '20%' }}>Judul</TableTh> <TableTh w="20%">
<TableTh style={{ width: '20%' }}>Kategori</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh> Judul
<TableTh style={{ width: '15%' }}>Detail</TableTh> </Text>
</TableTh>
<TableTh w="20%">
<Text fz="sm" fw={600} lh={1.4}>
Kategori
</Text>
</TableTh>
<TableTh w="35%">
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi
</Text>
</TableTh>
<TableTh w="15%">
<Text fz="sm" fw={600} lh={1.4}>
Detail
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -99,27 +121,23 @@ function ListPotensi({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" c="gray.7" lh={1.5}>
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text> {item.kategori?.nama || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={300}> <Text
<Text fz="sm"
lineClamp={1} lh={1.5}
truncate lineClamp={2}
fz="sm" dangerouslySetInnerHTML={{ __html: item.deskripsi }}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} style={{ wordBreak: 'break-word' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
/>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -138,8 +156,10 @@ function ListPotensi({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text color="dimmed">Tidak ada data potensi yang cocok</Text> <Text c="gray.6" fz="sm" ta="center" lh={1.5}>
Tidak ada data potensi yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -147,7 +167,64 @@ function ListPotensi({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
<Stack gap="sm">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Box mb="xs">
<Text fz="xs" fw={600} lh={1.4}>
Judul
</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.name}
</Text>
</Box>
<Box mb="xs">
<Text fz="xs" fw={600} lh={1.4}>
Kategori
</Text>
<Text fz="sm" c="gray.7" lh={1.5}>
{item.kategori?.nama || '-'}
</Text>
</Box>
<Box mb="xs">
<Text fz="xs" fw={600} lh={1.4}>
Deskripsi
</Text>
<Text
fz="sm"
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{ wordBreak: 'break-word' }}
/>
</Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
w="100%"
>
Detail
</Button>
</Paper>
))}
</Stack>
) : (
<Center py="xl">
<Text c="gray.6" fz="sm" ta="center" lh={1.5}>
Tidak ada data potensi yang cocok
</Text>
</Center>
)}
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -166,4 +243,4 @@ function ListPotensi({ search }: { search: string }) {
); );
} }
export default Potensi; export default Potensi;

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 { IconCalendar, IconUser, IconUsers } from '@tabler/icons-react'; import { IconCalendar, IconUser, 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';
@@ -60,35 +60,76 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
style={{ p="sm"
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", style={{
borderRadius: "1rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", borderRadius: "1rem",
display: "flex", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
flexWrap: "nowrap", display: "flex",
gap: "0.5rem", flexWrap: "nowrap",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi gap: "0.5rem",
}} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
> >
{tabs.map((tab, i) => (
<TabsTab <TabsList
key={i} p="xs" // lebih kecil
value={tab.value} style={{
leftSection={tab.icon} background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
style={{ borderRadius: "1rem",
fontWeight: 600, display: "flex",
fontSize: "0.9rem", flexWrap: "nowrap",
transition: "all 0.2s ease", gap: "0.5rem",
}} width: "max-content", // ⬅️ kunci
> maxWidth: "100%", // ⬅️ penting
{tab.label} }}
</TabsTab> >
))} {tabs.map((tab, i) => (
</TabsList> <TabsTab
</ScrollArea> key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -148,7 +148,7 @@ function Page() {
// ❌ Error // ❌ Error
if (loadError) { if (loadError) {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md"> <Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
@@ -166,7 +166,7 @@ function Page() {
// 🧱 UI utama // 🧱 UI utama
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md"> <Stack gap="md">
{/* Header */} {/* Header */}
<Group mb="sm"> <Group mb="sm">

View File

@@ -170,7 +170,7 @@ function Page() {
// Loading state // Loading state
if (maskotState.findUnique.loading || maskotState.update.loading) { if (maskotState.findUnique.loading || maskotState.update.loading) {
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Center h={400}> <Center h={400}>
<Text>Memuat data...</Text> <Text>Memuat data...</Text>
</Center> </Center>
@@ -181,7 +181,7 @@ function Page() {
// Error state // Error state
if (maskotState.findUnique.error) { if (maskotState.findUnique.error) {
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md"> <Stack gap="md">
<Button variant="subtle" onClick={handleBack}> <Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} /> <IconArrowBack color={colors['blue-button']} size={20} />
@@ -196,7 +196,7 @@ function Page() {
} }
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="xs"> <Stack gap="xs">
<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

@@ -91,7 +91,7 @@ function Page() {
}, [params?.id, router]); }, [params?.id, router]);
// 🔄 Check if form has changes // 🔄 Check if form has changes
// 🔁 Reset Form to Original Data // 🔁 Reset Form to Original Data
const handleResetForm = () => { const handleResetForm = () => {
@@ -149,7 +149,7 @@ function Page() {
// 🔄 Loading State // 🔄 Loading State
if (isLoading) { if (isLoading) {
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Center h={400}> <Center h={400}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} /> <Loader size="lg" color={colors['blue-button']} />
@@ -165,7 +165,7 @@ function Page() {
// ❌ Error State // ❌ Error State
if (loadError) { if (loadError) {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md"> <Stack gap="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

@@ -126,7 +126,7 @@ function Page() {
// ⏳ Loading // ⏳ Loading
if (isLoading) { if (isLoading) {
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Center h={400}> <Center h={400}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} /> <Loader size="lg" color={colors['blue-button']} />
@@ -142,7 +142,7 @@ function Page() {
// ❌ Error // ❌ Error
if (loadError) { if (loadError) {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md"> <Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Button variant="subtle" onClick={handleBack} 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, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Card, Center, Divider, Group, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -30,208 +30,558 @@ function Page() {
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title> <Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
{/* Sejarah Desa */} {/* Sejarah Desa */}
{sejarah && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Box px={{ base: 0, md: 50 }} py="xl"> <Box visibleFrom='md'>
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> {sejarah && (
<Stack gap={0}> <Paper p={{ base: "lg", md: "xl" }} bg={'white'} withBorder radius="md" shadow="xs">
<Center> <Group justify='space-between'>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" /> <Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
</Center> <Button
<Paper c="green"
bg={colors['blue-button']} variant="light"
py="md" leftSection={<IconEdit size={18} stroke={2} />}
px="sm" radius="md"
radius="md" onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }} >
> Edit
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}> </Button>
{sejarah.judul} </Group>
</Text>
</Paper> <Box py="xl">
</Stack> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Divider my="md" color={colors['blue-button']} /> <Stack gap={0}>
<Text fz={{ base: "md", md: "h3" }} style={{wordBreak: "break-word", whiteSpace: "normal"}} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} /> <Center>
</Paper> <Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Box> </Center>
</Paper> <Paper
)} bg={colors['blue-button']}
py="md"
px="sm"
radius="md"
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" }}>
{sejarah.judul}
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<Box px={20}>
<Text fz={{ base: "md", md: "h3" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
</Box>
</Paper>
</Box>
</Paper>
)}
</Box>
<Box hiddenFrom='md'>
{sejarah && (
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
{/* Header */}
<Group justify="space-between" align="center" mb="md">
<Title order={3} c={colors['blue-button']}>
Preview Sejarah Desa
</Title>
<Button
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={18} />}
onClick={() =>
router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)
}
>
Edit
</Button>
</Group>
{/* Content Wrapper */}
<Box
mx="auto"
w="100%"
maw={720} // batas nyaman baca
>
<Paper
bg={colors['white-1']}
withBorder
radius="md"
p={{ base: "md", md: "lg" }}
>
{/* Logo + Title */}
<Stack align="center" gap="xs">
<Image
src="/darmasaba-icon.png"
alt="Logo Desa"
w={{ base: 120, md: 200 }}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="md"
py="sm"
radius="md"
mt={{ base: "sm", md: -24 }} // aman di mobile
>
<Text
ta="center"
c="white"
fw={700}
fz={{ base: "lg", md: "xl" }}
>
{sejarah.judul}
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
{/* Deskripsi */}
<Box pl={10}>
<Text
fz="sm"
lh="1.6"
ta="left"
style={{ wordBreak: "break-word" }}
dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }}
/>
</Box>
</Paper>
</Box>
</Paper>
)}
</Box>
{/* Visi Misi Desa */} {/* Visi Misi Desa */}
{visiMisi && ( <Box visibleFrom='md'>
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> {visiMisi && (
<Grid align="center"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<GridCol span={{ base: 12, md: 11 }}> <Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
</GridCol> <Button
<GridCol span={{ base: 12, md: 1 }}> c="green"
<Button variant="light"
c="green" leftSection={<IconEdit size={18} stroke={2} />}
variant="light" radius="md"
leftSection={<IconEdit size={18} stroke={2} />} onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
radius="md" >
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)} Edit
> </Button>
Edit </Group>
</Button>
</GridCol>
</Grid>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Box px={{ base: 0, md: 50 }} py="xl">
<Stack gap={0}> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Center> <Stack gap={0}>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" /> <Center>
</Center> <Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
<Paper </Center>
bg={colors['blue-button']} <Paper
py="md" bg={colors['blue-button']}
px="sm" py="md"
radius="md" px="sm"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }} radius="md"
> 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" }}> >
Visi Misi Desa <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
</Text> Visi Misi Desa
</Paper> </Text>
</Stack> </Paper>
<Divider my="md" color={colors['blue-button']} /> </Stack>
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text> <Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} /> <Box px={20}>
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text> <Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} /> <Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
</Paper> </Box>
</Box> <Box px={20}>
</Paper> <Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
)} <Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
</Box>
</Paper>
</Box>
</Paper>
)}
</Box>
<Box hiddenFrom='md'>
{visiMisi && (
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
{/* Header */}
<Group justify="space-between" align="center" mb="md">
<Title order={3} c={colors['blue-button']}>
Preview Visi Misi Desa
</Title>
<Button
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={18} />}
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
>
Edit
</Button>
</Group>
{/* Content Wrapper */}
<Box
mx="auto"
w="100%"
maw={720} // batas nyaman baca
>
<Paper
bg={colors['white-1']}
withBorder
radius="md"
p={{ base: "md", md: "lg" }}
>
{/* Logo + Title */}
<Stack align="center" gap="xs">
<Image
src="/darmasaba-icon.png"
alt="Logo Desa"
w={{ base: 120, md: 200 }}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="md"
py="sm"
radius="md"
mt={{ base: "sm", md: -24 }} // aman di mobile
>
<Text
ta="center"
c="white"
fw={700}
fz={{ base: "lg", md: "xl" }}
>
Visi Misi Desa
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<Stack pl={10}>
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
<Text
fz="sm"
lh="1.6"
ta="left"
style={{ wordBreak: "break-word" }}
dangerouslySetInnerHTML={{ __html: visiMisi.visi }}
/>
</Stack>
<Stack pl={10}>
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
<Text
fz="sm"
lh="1.6"
ta="left"
style={{ wordBreak: "break-word" }}
dangerouslySetInnerHTML={{ __html: visiMisi.misi }}
/>
</Stack>
</Paper>
</Box>
</Paper>
)}
</Box>
{/* Lambang Desa */} {/* Lambang Desa */}
{lambang && ( <Box visibleFrom='md'>
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> {lambang && (
<Grid align="center"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<GridCol span={{ base: 12, md: 11 }}> <Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
</GridCol> <Button
<GridCol span={{ base: 12, md: 1 }}> c="green"
<Button variant="light"
c="green" leftSection={<IconEdit size={18} stroke={2} />}
variant="light" radius="md"
leftSection={<IconEdit size={18} stroke={2} />} onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
radius="md" >
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)} Edit
> </Button>
Edit </Group>
</Button>
</GridCol>
</Grid>
<Box px={{ base: 0, md: 50 }} py="xl"> <Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" /> <Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
py="md" py="md"
px="sm" px="sm"
radius="md" radius="md"
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" }}> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
{lambang.judul} {lambang.judul}
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>
<Divider my="md" color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} /> <Box px={20}>
</Paper> <Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
</Box> </Box>
</Paper> </Paper>
)} </Box>
</Paper>
)}
</Box>
<Box hiddenFrom='md'>
{lambang && (
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
{/* Header */}
<Group justify="space-between" align="center" mb="md">
<Title order={3} c={colors['blue-button']}>
Preview Lambang Desa
</Title>
<Button
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={18} />}
onClick={() =>
router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)
}
>
Edit
</Button>
</Group>
{/* Content Wrapper */}
<Box
mx="auto"
w="100%"
maw={720} // batas nyaman baca
>
<Paper
bg={colors['white-1']}
withBorder
radius="md"
p={{ base: "md", md: "lg" }}
>
{/* Logo + Title */}
<Stack align="center" gap="xs">
<Image
src="/darmasaba-icon.png"
alt="Logo Desa"
w={{ base: 120, md: 200 }}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="md"
py="sm"
radius="md"
mt={{ base: "sm", md: -24 }} // aman di mobile
>
<Text
ta="center"
c="white"
fw={700}
fz={{ base: "lg", md: "xl" }}
>
{lambang.judul}
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
{/* Deskripsi */}
<Box pl={10}>
<Text
fz="sm"
lh="1.6"
ta="left"
style={{ wordBreak: "break-word" }}
dangerouslySetInnerHTML={{ __html: lambang.deskripsi }}
/>
</Box>
</Paper>
</Box>
</Paper>
)}
</Box>
{/* Maskot Desa */} {/* Maskot Desa */}
{maskot && ( <Box visibleFrom='md'>
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> {maskot && (
<Grid align="center"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<GridCol span={{ base: 12, md: 11 }}> <Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
</GridCol> <Button
<GridCol span={{ base: 12, md: 1 }}> c="green"
<Button variant="light"
c="green" leftSection={<IconEdit size={18} stroke={2} />}
variant="light" radius="md"
leftSection={<IconEdit size={18} stroke={2} />} onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
radius="md" >
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)} Edit
> </Button>
Edit </Group>
</Button>
</GridCol>
</Grid>
<Box px={{ base: 0, md: 50 }} py="xl"> <Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" /> <Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
py="md" py="md"
px="sm" px="sm"
radius="md" radius="md"
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" }}> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
Maskot Desa Maskot Desa
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>
<Divider my="md" color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} /> <Box px={20}>
<Stack mt="md" gap="sm"> <Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="md"> </Box>
{maskot.images.map((img, idx) => ( <Stack mt="md" gap="sm">
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}> <SimpleGrid cols={{ base: 1, md: 4 }} spacing="md">
<Center> {maskot.images.map((img, idx) => (
<Image <Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
src={img.image.link} <Center>
alt={img.label} <Image
w={150} src={img.image.link}
h={150} alt={img.label}
fit="cover" w={150}
radius="md" h={150}
style={{ border: '1px solid #ccc' }} fit="cover"
loading='lazy' radius="md"
/> style={{ border: '1px solid #ccc' }}
</Center> loading='lazy'
<Text ta="center" mt="xs" fw="bold">{img.label}</Text> />
</Card> </Center>
))} <Text ta="center" mt="xs" fw="bold">{img.label}</Text>
</SimpleGrid> </Card>
</Stack> ))}
</Paper> </SimpleGrid>
</Box> </Stack>
</Paper> </Paper>
)} </Box>
</Paper>
)}
</Box>
<Box hiddenFrom='md'>
{maskot && (
<Paper p={{ base: "md", md: "xl" }} bg="white" withBorder radius="md" shadow="xs">
{/* Header */}
<Group justify="space-between" align="center" mb="md">
<Title order={3} c={colors['blue-button']}>
Preview Maskot Desa
</Title>
<Button
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={18} />}
onClick={() =>
router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)
}
>
Edit
</Button>
</Group>
{/* Content Wrapper */}
<Box
mx="auto"
w="100%"
maw={720} // batas nyaman baca
>
<Paper
bg={colors['white-1']}
withBorder
radius="md"
p={{ base: "md", md: "lg" }}
>
{/* Logo + Title */}
<Stack align="center" gap="xs">
<Image
src="/pudak-icon.png"
alt="Logo Desa"
w={{ base: 120, md: 200 }}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="md"
py="sm"
radius="md"
mt={{ base: "sm", md: -24 }} // aman di mobile
>
<Text
ta="center"
c="white"
fw={700}
fz={{ base: "lg", md: "xl" }}
>
Maskot Desa
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
{/* Deskripsi */}
<Box pl={10}>
<Text
fz="sm"
lh="1.6"
ta="left"
style={{ wordBreak: "break-word" }}
dangerouslySetInnerHTML={{ __html: maskot.deskripsi }}
/>
</Box>
<Stack mt="md" gap="sm">
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="md">
{maskot.images.map((img, idx) => (
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
<Center>
<Image
src={img.image.link}
alt={img.label}
w={150}
h={150}
fit="cover"
radius="md"
style={{ border: '1px solid #ccc' }}
loading='lazy'
/>
</Center>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
</Card>
))}
</SimpleGrid>
</Stack>
</Paper>
</Box>
</Paper>
)}
</Box>
</Stack> </Stack>
</Paper> </Paper>
); );

View File

@@ -127,7 +127,7 @@ function EditPerbekelDariMasaKeMasa() {
}; };
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

@@ -40,7 +40,7 @@ function DetailPerbekelDariMasa() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -52,7 +52,7 @@ function DetailPerbekelDariMasa() {
<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

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

View File

@@ -1,7 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect } from '@mantine/hooks'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { 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,43 +46,47 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
const state = useProxy(stateProfileDesa.mantanPerbekel) const state = useProxy(stateProfileDesa.mantanPerbekel)
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify='space-between' mb="md"> <Group justify='space-between' mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Perbekel Dari Masa Ke Masa</Title> <Title order={4} lh={1.2}>
<Button List Perbekel Dari Masa Ke Masa
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')} color="blue"
> variant="light"
Tambah Baru onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
</Button> >
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '35%' }}>Nama Perbekel</TableTh> <TableTh fz="sm" fw={600} ta="left" c="dark.9">Nama Perbekel</TableTh>
<TableTh style={{ width: '35%' }}>Periode</TableTh> <TableTh fz="sm" fw={600} ta="left" c="dark.9">Periode</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh> <TableTh fz="sm" fw={600} ta="left" c="dark.9">Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -73,36 +94,32 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} lineClamp={1}>{item.nama}</Text>
<Text fw={500} lineClamp={1}>{item.nama}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="md" lh={1.5} lineClamp={1}>{item.periode}</Text>
<Text lineClamp={1}>{item.periode}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <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/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)} >
> Detail
Detail </Button>
</Button>
</Box>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py={{ base: 'md', md: 'lg' }}>
<Text color="dimmed">Tidak ada data perbekel yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data perbekel yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -110,6 +127,47 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={'xs'}>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark.9">Nama Perbekel</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.nama}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark.9">Periode</Text>
<Text fz="sm" lh={1.5}>{item.periode}</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
fullWidth
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="md">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data perbekel yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
@@ -120,8 +178,8 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt={{ base: 'sm', md: 'md' }}
mb="md" mb={{ base: 'sm', md: 'md' }}
color="blue" color="blue"
radius="md" radius="md"
/> />
@@ -130,4 +188,4 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
); );
} }
export default PerbekelDariMasaKeMasa; export default PerbekelDariMasaKeMasa;

View File

@@ -97,7 +97,7 @@ function ProfilePerbekel() {
} }
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="xs"> <Stack gap="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">

View File

@@ -33,18 +33,18 @@ function Page() {
{/* Header + tombol edit */} {/* Header + tombol edit */}
<Grid align="center"> <Grid align="center">
<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={2} c={colors['blue-button']} lh={1.2} />
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
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/desa/profil/profil-perbekel/${perbekel.id}`)} onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
> >
Edit Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
@@ -58,7 +58,13 @@ 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']}> <Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
fw="bold"
c={colors['blue-button']}
lh={{ base: 1.45, md: 1.45 }}
>
Profil Pimpinan Badan Publik Desa Darmasaba Profil Pimpinan Badan Publik Desa Darmasaba
</Text> </Text>
</GridCol> </GridCol>
@@ -86,25 +92,55 @@ 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" }}> <Text
I.B. Surya Prabhawa Manuaba, S.H., M.H. ta="center"
c={colors['white-1']}
fw="bolder"
fz={{ base: 'md', md: 'lg' }}
lh={{ base: 1.4, md: 1.4 }}
>
I.B. Surya Prabhawa Manuaba, S.H., M.H.
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>
{/* Biodata & Info */} {/* Biodata & Info */}
<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" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} /> <Text
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.6 }}
ta="justify"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: perbekel.biodata }}
/>
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman</Text> <Title order={3} lh={1.2} mt="md" mb={4} />
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} /> <Text
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.6 }}
ta="justify"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }}
/>
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman Organisasi</Text> <Title order={3} lh={1.2} mt="md" mb={4} />
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} /> <Text
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.6 }}
ta="justify"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }}
/>
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Program Kerja Unggulan</Text> <Title order={3} lh={1.2} mt="md" mb={4} />
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} /> <Text
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.6 }}
ta="justify"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }}
/>
</Box> </Box>
</Paper> </Paper>
</Stack> </Stack>
@@ -112,4 +148,4 @@ function Page() {
); );
} }
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 { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react'; import { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } 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';
@@ -81,52 +81,93 @@ 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

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { 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';
@@ -50,10 +50,11 @@ function ListArtikelKesehatan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = stateArtikel.findMany; const { data, page, totalPages, loading, load } = stateArtikel.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];

View File

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

View File

@@ -56,7 +56,7 @@ function DetailFasilitasKesehatan() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Back */} {/* Tombol Back */}
<Button <Button
variant="subtle" variant="subtle"
@@ -83,12 +83,12 @@ function DetailFasilitasKesehatan() {
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box> <Box pl={10}>
<Text fz="lg" fw="bold">Nama Fasilitas</Text> <Text fz="lg" fw="bold">Nama Fasilitas</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box pl={10}>
<Text fz="lg" fw="bold">Informasi Umum</Text> <Text fz="lg" fw="bold">Informasi Umum</Text>
<Text fz="md" fw="bold">Fasilitas</Text> <Text fz="md" fw="bold">Fasilitas</Text>
<Text fz="md" c="dimmed">{data.informasiumum?.fasilitas || '-'}</Text> <Text fz="md" c="dimmed">{data.informasiumum?.fasilitas || '-'}</Text>
@@ -98,22 +98,22 @@ function DetailFasilitasKesehatan() {
<Text fz="md" c="dimmed">{data.informasiumum?.jamOperasional || '-'}</Text> <Text fz="md" c="dimmed">{data.informasiumum?.jamOperasional || '-'}</Text>
</Box> </Box>
<Box> <Box pl={10}>
<Text fz="lg" fw="bold">Layanan Unggulan</Text> <Text fz="lg" fw="bold">Layanan Unggulan</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.layananunggulan?.content || '-' }} /> <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.layananunggulan?.content || '-' }} />
</Box> </Box>
<Box> <Box pl={10}>
<Text fz="lg" fw="bold">Fasilitas Pendukung</Text> <Text fz="lg" fw="bold">Fasilitas Pendukung</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.fasilitaspendukung?.content || '-' }} /> <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.fasilitaspendukung?.content || '-' }} />
</Box> </Box>
<Box> <Box pl={10}>
<Text fz="lg" fw="bold">Prosedur Pendaftaran</Text> <Text fz="lg" fw="bold">Prosedur Pendaftaran</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.prosedurpendaftaran?.content || '-' }} /> <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.prosedurpendaftaran?.content || '-' }} />
</Box> </Box>
<Box> <Box pl={10}>
<Text fz="lg" fw="bold" mb="sm">Dokter & Tenaga Medis</Text> <Text fz="lg" fw="bold" mb="sm">Dokter & Tenaga Medis</Text>
{Array.isArray(data.dokterdantenagamedis) && data.dokterdantenagamedis.length > 0 ? ( {Array.isArray(data.dokterdantenagamedis) && data.dokterdantenagamedis.length > 0 ? (
<Box style={{ overflowX: 'auto', width: '100%' }}> <Box style={{ overflowX: 'auto', width: '100%' }}>
@@ -159,7 +159,7 @@ function DetailFasilitasKesehatan() {
)} )}
</Box> </Box>
<Box mt="xl"> <Box pl={10} mt="xl">
<Text fz="lg" fw="bold" mb="sm">Tarif & Layanan</Text> <Text fz="lg" fw="bold" mb="sm">Tarif & Layanan</Text>
{Array.isArray(data.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? ( {Array.isArray(data.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? (
<Box style={{ overflowX: 'auto', width: '100%' }}> <Box style={{ overflowX: 'auto', width: '100%' }}>

View File

@@ -70,7 +70,7 @@ function CreateFasilitasKesehatan() {
}, []); }, []);
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}> <Box px={{ base: 0, md: 'xs' }} py="xs" component="form" onSubmit={handleSubmit}>
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

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

View File

@@ -50,7 +50,7 @@ function DetailDokterTenagaMedis() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Back */} {/* Tombol Back */}
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -56,7 +56,7 @@ function CreateDokter() {
} }
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}> <Box px={{ base: 0, md: 'lg' }} py="xs" component="form" onSubmit={handleSubmit}>
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<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, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -10,13 +10,12 @@ import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import { useState } from 'react'; import { useState } from 'react';
function DokterTenagaMedis() { function DokterTenagaMedis() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter(); const router = useRouter();
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb="sm">
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}> <Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
@@ -44,25 +43,29 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
totalPages totalPages
} = stateFasilitasKesehatan.findMany } = stateFasilitasKesehatan.findMany
const [debouncedSearch] = useDebouncedValue(search, 1000);
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 (
<Box py={10}> <Box py="md">
<Skeleton h={500} /> <Skeleton h={500} />
</Box> </Box>
) )
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Dokter dan Tenaga Medis</Title> <Title order={3} visibleFrom="md">Daftar Dokter dan Tenaga Medis</Title>
<Title order={4} hiddenFrom="md">Daftar Dokter dan Tenaga Medis</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -77,15 +80,15 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Tabel */} {/* Desktop Table */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Dokter</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Nama Dokter</Text></TableTh>
<TableTh>Spesialis</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Spesialis</Text></TableTh>
<TableTh>Jadwal</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Jadwal</Text></TableTh>
<TableTh>Aksi</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -93,21 +96,17 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
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" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5}>
{item.specialist || '-'} {item.specialist || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5} dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
<Text dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -120,7 +119,7 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
} }
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml="xs" fz="sm" fw={500}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -128,8 +127,8 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text c="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok Tidak ada fasilitas kesehatan yang cocok
</Text> </Text>
</Center> </Center>
@@ -139,6 +138,47 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" mb="xs" radius="sm">
<Box mb="xs">
<Text fz="sm" fw={600} lh={1.4}>Nama Dokter</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.name}</Text>
</Box>
<Box mb="xs">
<Text fz="sm" fw={600} lh={1.4}>Spesialis</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.specialist || '-'}</Text>
</Box>
<Box mb="md">
<Text fz="sm" fw={600} lh={1.4}>Jadwal</Text>
<Text fz="sm" fw={500} lh={1.5} dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
</Box>
<Button
fullWidth
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml="xs" fz="sm" fw={500}>Detail</Text>
</Button>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
)}
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -160,4 +200,4 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
) )
} }
export default DokterTenagaMedis; export default DokterTenagaMedis;

View File

@@ -5,8 +5,6 @@ import {
Box, Box,
Button, Button,
Center, Center,
Grid,
GridCol,
Group, Group,
Pagination, Pagination,
Paper, Paper,
@@ -23,82 +21,138 @@ import {
Title, Title,
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconCoin, IconDeviceImacCog, IconPlus, IconReportMedical, IconSearch } from '@tabler/icons-react'; import { IconCoin, IconDeviceImacCog, IconPlus, IconReportMedical, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
function FasilitasKesehatan() { function FasilitasKesehatan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter() const router = useRouter();
return ( return (
<Box> <Box>
<Grid mb={10}> <Paper p="lg" radius="md" mb="lg" bg={colors['white-1']} shadow="sm">
<GridCol span={{ base: 12, md: 8 }}> <Group justify='space-between' visibleFrom='md'>
<Title order={3}>Fasilitas Kesehatan</Title> <Title order={2} visibleFrom="md" size="lg" lh={1.2}>
</GridCol> Fasilitas Kesehatan
<GridCol span={{ base: 12, md: 4 }}> </Title>
<Group gap={"xs"}> <Title order={2} hiddenFrom="md" size="md" lh={1.2}>
Fasilitas Kesehatan
</Title>
<Group gap="xs" justify="flex-end">
<Tooltip label="List Dokter" withArrow> <Tooltip label="List Dokter" withArrow>
<ActionIcon onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')} size="lg" radius="xl" color="green.6"> <ActionIcon
onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')}
size="lg"
radius="xl"
color="green.6"
>
<IconReportMedical size={20} /> <IconReportMedical size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="List Tarif Layanan" withArrow> <Tooltip label="List Tarif Layanan" withArrow>
<ActionIcon onClick={()=> router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')} size="lg" radius="xl" color="blue.6"> <ActionIcon
onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')}
size="lg"
radius="xl"
color="blue.6"
>
<IconCoin size={20} /> <IconCoin size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput <TextInput
radius="lg" radius="lg"
placeholder='Cari nama, alamat, atau jam operasional...' placeholder='Cari nama, alamat, atau jam operasional...'
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
w="133%"
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
fz={{ base: 'xs', md: 'sm' }}
px="sm"
py="xs"
/> />
</Paper>
</Group> </Group>
</GridCol> </Group>
</Grid>
<Group justify='space-between' hiddenFrom='md'>
<Title order={2} visibleFrom="md" size="lg" lh={1.2}>
Fasilitas Kesehatan
</Title>
<Title order={2} hiddenFrom="md" size="md" lh={1.2}>
Fasilitas Kesehatan
</Title>
<Group gap="xs" justify="flex-start">
<Tooltip label="List Dokter" withArrow>
<ActionIcon
onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')}
size="lg"
radius="xl"
color="green.6"
>
<IconReportMedical size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label="List Tarif Layanan" withArrow>
<ActionIcon
onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')}
size="lg"
radius="xl"
color="blue.6"
>
<IconCoin size={20} />
</ActionIcon>
</Tooltip>
<TextInput
radius="lg"
placeholder='Cari nama, alamat, atau jam operasional...'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
fz={{ base: 'xs', md: 'sm' }}
px="sm"
py="xs"
/>
</Group>
</Group>
</Paper>
<ListFasilitasKesehatan search={search} /> <ListFasilitasKesehatan search={search} />
</Box> </Box>
); );
} }
function ListFasilitasKesehatan({ search }: { search: string }) { function ListFasilitasKesehatan({ search }: { search: string }) {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany; const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, debouncedSearch);
load(page, 10, search); }, [page, debouncedSearch]);
}, [page, search]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Fasilitas Kesehatan</Title> <Title order={3} visibleFrom="md" size="md" lh={1.2}>
Daftar Fasilitas Kesehatan
</Title>
<Title order={3} hiddenFrom="md" size="sm" lh={1.2}>
Daftar Fasilitas Kesehatan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -108,13 +162,15 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create' '/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create'
) )
} }
fz={{ base: 'sm', md: 'md' }}
px="sm"
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
{/* Tabel */} {/* Desktop Table */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
@@ -129,27 +185,23 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
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" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5}>
{item.dokterdantenagamedis?.length {item.dokterdantenagamedis?.length
? `${item.dokterdantenagamedis.length} dokter` ? `${item.dokterdantenagamedis.length} dokter`
: '-'} : '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5}>
<Text truncate="end" lineClamp={1}> {item.tarifdanlayanan?.length
{item.tarifdanlayanan?.length ? `${item.tarifdanlayanan.length} layanan`
? `${item.tarifdanlayanan.length} layanan` : '-'}
: '-'} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -160,6 +212,9 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}` `/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`
) )
} }
fz="sm"
px="sm"
h={36}
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml={5}>Detail</Text>
@@ -170,8 +225,8 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text c="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok Tidak ada fasilitas kesehatan yang cocok
</Text> </Text>
</Center> </Center>
@@ -181,6 +236,65 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Box mb="xs">
<Text fz="xs" fw={600} lh={1.4}>
Fasilitas Kesehatan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box mb="xs">
<Text fz="xs" fw={600} lh={1.4}>
Jumlah Dokter
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.dokterdantenagamedis?.length
? `${item.dokterdantenagamedis.length} dokter`
: '-'}
</Text>
</Box>
<Box mb="xs">
<Text fz="xs" fw={600} lh={1.4}>
Jumlah Layanan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tarifdanlayanan?.length
? `${item.tarifdanlayanan.length} layanan`
: '-'}
</Text>
</Box>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`
)
}
fullWidth
fz="sm"
mt="md"
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -199,7 +313,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default FasilitasKesehatan; export default FasilitasKesehatan;

View File

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

View File

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

View File

@@ -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, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -11,13 +11,12 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import { useState } from 'react'; import { useState } from 'react';
function TarifLayanan() { function TarifLayanan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter(); const router = useRouter();
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb="sm">
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}> <Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
@@ -39,6 +38,7 @@ function ListTarifLayanan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 10000);
const { const {
data, data,
@@ -46,36 +46,39 @@ function ListTarifLayanan({ search }: { search: string }) {
load, load,
page, page,
totalPages totalPages
} = stateFasilitasKesehatan.findMany } = stateFasilitasKesehatan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch);
}, [page, search]) }, [page, debouncedSearch]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
stateFasilitasKesehatan.delete.byId(selectedId); stateFasilitasKesehatan.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Box py="lg">
<Skeleton h={500} /> <Skeleton h={500} />
</Box> </Box>
) );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Tarif dan Layanan</Title> <Title order={4} lh={1.2}>
Daftar Tarif dan Layanan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -90,15 +93,31 @@ function ListTarifLayanan({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Tabel */} {/* Desktop Table */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Layanan</TableTh> <TableTh>
<TableTh>Tarif</TableTh> <Text fz="sm" fw={600} lh={1.4} ta="left">
<TableTh>Edit</TableTh> Layanan
<TableTh>Hapus</TableTh> </Text>
</TableTh>
<TableTh>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Tarif
</Text>
</TableTh>
<TableTh>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Edit
</Text>
</TableTh>
<TableTh>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Hapus
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -106,18 +125,16 @@ function ListTarifLayanan({ search }: { search: string }) {
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" lineClamp={1}>
{item.layanan || '-'} {item.layanan || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.tarif}
{item.tarif} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd ta="center">
<Button <Button
variant="light" variant="light"
color="green" color="green"
@@ -126,11 +143,12 @@ function ListTarifLayanan({ search }: { search: string }) {
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/${item.id}` `/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/${item.id}`
) )
} }
size="compact-sm"
> >
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd ta="center">
<Button <Button
variant="light" variant="light"
color="red" color="red"
@@ -139,6 +157,7 @@ function ListTarifLayanan({ search }: { search: string }) {
setSelectedId(item.id); setSelectedId(item.id);
setModalHapus(true); setModalHapus(true);
}} }}
size="compact-sm"
> >
<IconTrash size={18} /> <IconTrash size={18} />
</Button> </Button>
@@ -148,8 +167,8 @@ function ListTarifLayanan({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="lg">
<Text c="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok Tidak ada fasilitas kesehatan yang cocok
</Text> </Text>
</Center> </Center>
@@ -159,6 +178,64 @@ function ListTarifLayanan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" mb="xs" radius="sm">
<Box mb="xs">
<Text fz="sm" fw={600} lh={1.4}>
Layanan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.layanan || '-'}
</Text>
</Box>
<Box mb="md">
<Text fz="sm" fw={600} lh={1.4}>
Tarif
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tarif}
</Text>
</Box>
<Group justify="center" gap="xs">
<Button
variant="light"
color="green"
size="compact-xs"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="compact-xs"
disabled={stateFasilitasKesehatan.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Paper>
))
) : (
<Center py="lg">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
)}
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -176,6 +253,7 @@ function ListTarifLayanan({ search }: { search: string }) {
radius="md" radius="md"
/> />
</Center> </Center>
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
@@ -183,7 +261,7 @@ function ListTarifLayanan({ search }: { search: string }) {
text="Apakah anda yakin ingin menghapus tarif layanan ini?" text="Apakah anda yakin ingin menghapus tarif layanan ini?"
/> />
</Box> </Box>
) );
} }
export default TarifLayanan; export default TarifLayanan;

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { 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';
@@ -50,23 +50,24 @@ function ListJadwalKegiatan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
@@ -83,16 +84,16 @@ function ListJadwalKegiatan({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Tabel */} {/* Desktop Table */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh fz="sm" fw={600} lh={1.2}>Nama</TableTh>
<TableTh>Tanggal</TableTh> <TableTh fz="sm" fw={600} lh={1.2}>Tanggal</TableTh>
<TableTh>Waktu</TableTh> <TableTh fz="sm" fw={600} lh={1.2}>Waktu</TableTh>
<TableTh>Lokasi</TableTh> <TableTh fz="sm" fw={600} lh={1.2}>Lokasi</TableTh>
<TableTh>Aksi</TableTh> <TableTh fz="sm" fw={600} lh={1.2}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -100,14 +101,12 @@ function ListJadwalKegiatan({ search }: { search: string }) {
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" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.informasijadwalkegiatan.name}
{item.informasijadwalkegiatan.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5}>
{new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString(
'id-ID', 'id-ID',
{ {
@@ -116,19 +115,17 @@ function ListJadwalKegiatan({ search }: { search: string }) {
year: 'numeric', year: 'numeric',
} }
)} )}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5}>
{item.informasijadwalkegiatan.waktu} {item.informasijadwalkegiatan.waktu}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5} c="dimmed" truncate="end">
<Text truncate fz="sm" c="dimmed"> {item.informasijadwalkegiatan.lokasi}
{item.informasijadwalkegiatan.lokasi} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -141,7 +138,9 @@ function ListJadwalKegiatan({ search }: { search: string }) {
} }
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.5}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -149,8 +148,8 @@ function ListJadwalKegiatan({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jadwal kegiatan yang cocok Tidak ada jadwal kegiatan yang cocok
</Text> </Text>
</Center> </Center>
@@ -160,6 +159,72 @@ function ListJadwalKegiatan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.informasijadwalkegiatan.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Tanggal</Text>
<Text fz="sm" fw={500} lh={1.5}>
{new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString(
'id-ID',
{
day: '2-digit',
month: 'long',
year: 'numeric',
}
)}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Waktu</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.informasijadwalkegiatan.waktu}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Lokasi</Text>
<Text fz="sm" fw={500} lh={1.5} c="dimmed">
{item.informasijadwalkegiatan.lokasi}
</Text>
</Box>
<Box>
<Button
fullWidth
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5} fz="sm" fw={500} lh={1.5}>
Detail
</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jadwal kegiatan yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -181,4 +246,4 @@ function ListJadwalKegiatan({ search }: { search: string }) {
); );
} }
export default JadwalKegiatan; export default JadwalKegiatan;

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* 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 { import {
Box, Box,
@@ -18,26 +18,32 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } 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 { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Bar, BarChart, Tooltip as ChartTooltip, Legend, XAxis, YAxis } from 'recharts'; import {
Bar,
BarChart,
Tooltip as ChartTooltip,
Legend,
XAxis,
YAxis,
} from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
function GrafikHasilKepuasanMasyarakat() { function GrafikHasilKepuasanMasyarakat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Penderita Penyakit' title="Penderita Penyakit"
placeholder='Cari nama atau alamat...' placeholder="Cari nama atau alamat..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -59,9 +65,9 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
}; };
const stateGrafikKepuasan = useProxy(grafikkepuasan); const stateGrafikKepuasan = useProxy(grafikkepuasan);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [chartData, setChartData] = useState<PDKMGrafik[]>([]); const [chartData, setChartData] = useState<PDKMGrafik[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const isTablet = useMediaQuery('(max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
const router = useRouter(); const router = useRouter();
@@ -69,21 +75,26 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
setChartData(data.map((item) => ({ setChartData(
...item, data.map((item) => ({
tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal ...item,
}))); tanggal:
item.tanggal instanceof Date
? item.tanggal.toISOString()
: item.tanggal,
}))
);
} }
}, [data]); }, [data]);
const processDiseaseData = (data: PDKMGrafik[]) => { const processDiseaseData = (data: PDKMGrafik[]) => {
const diseaseCount: Record<string, number> = {}; const diseaseCount: Record<string, number> = {};
data.forEach(item => { data.forEach((item) => {
const penyakit = item.penyakit.trim(); const penyakit = item.penyakit.trim();
if (penyakit) { if (penyakit) {
diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1; diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1;
@@ -92,7 +103,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
return Object.entries(diseaseCount).map(([name, count]) => ({ name, count })); return Object.entries(diseaseCount).map(([name, count]) => ({ name, count }));
}; };
const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]); const [diseaseChartData, setDiseaseChartData] = useState<{ name: string; count: number }[]>([]);
useEffect(() => { useEffect(() => {
if (data && data.length > 0) { if (data && data.length > 0) {
@@ -104,18 +115,23 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack gap='lg' py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> {/* Daftar Penderita Penyakit */}
{/* Judul + Tombol Tambah */} <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 Penderita Penyakit</Title> <Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Daftar Penderita Penyakit
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -130,8 +146,8 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Tabel */} {/* Desktop Table */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
@@ -146,29 +162,21 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd fz="md" fw={500} lh={1.5}>
<Box w={150}> {item.nama}
{item.nama}
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd fz="md" fw={500} lh={1.5}>
<Box w={150}> {new Date(item.tanggal).toLocaleDateString('id-ID', {
{new Date(item.tanggal).toLocaleDateString('id-ID', { day: '2-digit',
day: '2-digit', month: 'long',
month: 'long', year: 'numeric',
year: 'numeric', })}
})}
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd fz="md" fw={500} lh={1.5}>
<Box w={150}> {item.jenisKelamin}
{item.jenisKelamin}
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd fz="md" fw={500} lh={1.5}>
<Box w={150}> {item.penyakit}
{item.penyakit}
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -181,7 +189,9 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
} }
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -190,7 +200,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py={20}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kepuasan masyarakat yang cocok Tidak ada data kepuasan masyarakat yang cocok
</Text> </Text>
</Center> </Center>
@@ -200,6 +210,72 @@ function ListGrafikHasilKepuasanMasyarakat({ 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}>
<Text fz="sm" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
<Text fz="sm" fw={600} lh={1.4}>
Tanggal
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text>
<Text fz="sm" fw={600} lh={1.4}>
Jenis Kelamin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jenisKelamin}
</Text>
<Text fz="sm" fw={600} lh={1.4}>
Penyakit
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.penyakit}
</Text>
<Button
variant="light"
color="blue"
fullWidth
mt="xs"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5} fz="sm" fw={500}>
Detail
</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kepuasan masyarakat yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -218,38 +294,47 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
/> />
</Center> </Center>
{/* Chart */} {/* Chart Section */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Paper withBorder bg={colors['white-1']} p={'md'}> <Title
<Title pb={10} order={4}>Penderita Penyakit</Title> order={2}
{mounted && diseaseChartData.length > 0 ? ( lh={{ base: 1.2, md: 1.15 }}
<Center> mb={{ base: 'sm', md: 'md' }}
<BarChart >
width={isMobile ? 320 : isTablet ? 600 : 800} // kecilin biar muat Penderita Penyakit
height={350} </Title>
data={diseaseChartData}
> {mounted && diseaseChartData.length > 0 ? (
<XAxis <Center>
dataKey="name" <BarChart
tick={{ fontSize: 12 }} width={isMobile ? 320 : 800}
interval={0} height={350}
angle={-45} data={diseaseChartData}
textAnchor="end" >
height={70} <XAxis
/> dataKey="name"
<YAxis /> tick={{ fontSize: 12 }}
<ChartTooltip /> interval={0}
<Legend /> angle={-45}
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" /> textAnchor="end"
</BarChart> height={70}
</Center> />
) : ( <YAxis />
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <ChartTooltip />
)} <Legend />
</Paper> <Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</Box> </BarChart>
</Box> </Center>
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
</Center>
)}
</Paper>
</Stack>
); );
} }
export default GrafikHasilKepuasanMasyarakat; export default GrafikHasilKepuasanMasyarakat;

View File

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

View File

@@ -50,7 +50,7 @@ function DetailKelahiran() {
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Back */} {/* Tombol Back */}
<Button <Button
variant="subtle" variant="subtle"
@@ -65,7 +65,7 @@ function DetailKelahiran() {
{/* Wrapper Detail */} {/* Wrapper Detail */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "50%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

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

View File

@@ -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 { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, 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';
@@ -31,17 +31,15 @@ function Kelahiran() {
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Tombol Back */} {/* Tombol Back */}
<Box mb={10}> <Box mb="sm">
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian')}> <Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian')}>
<IconArrowBack color={colors["blue-button"]} size={25} /> <IconArrowBack color={colors["blue-button"]} size={25} />
</Button> </Button>
</Box> </Box>
{/* Header Search */} {/* Header Search */}
<HeaderSearch <HeaderSearch
title='Data Kelahiran' title='Data Kelahiran'
@@ -51,7 +49,6 @@ function Kelahiran() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKelahiran search={search} /> <ListKelahiran search={search} />
</Box> </Box>
); );
@@ -61,34 +58,32 @@ function Kelahiran() {
function ListKelahiran({ search }: { search: string }) { function ListKelahiran({ search }: { search: string }) {
const statePersentase = useProxy(persentasekelahiran.kelahiran); const statePersentase = useProxy(persentasekelahiran.kelahiran);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = statePersentase.findMany; const { data, page, totalPages, loading, load } = statePersentase.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack py="md" gap="xl">
<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">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="lg">
<Title order={4}>Daftar Data Kelahiran</Title> <Title order={2} lh={1.2}>
Daftar Data Kelahiran
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -103,17 +98,16 @@ function ListKelahiran({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Desktop Table */}
{/* Tabel */} <Box visibleFrom="md">
<Box style={{ overflowX: "auto" }}> <Table highlightOnHover fz="md">
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Nama</Text></TableTh>
<TableTh>Tanggal</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Tanggal</Text></TableTh>
<TableTh>Jenis Kelamin</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Jenis Kelamin</Text></TableTh>
<TableTh>Alamat</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Alamat</Text></TableTh>
<TableTh>Aksi</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -121,32 +115,28 @@ function ListKelahiran({ search }: { search: string }) {
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" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.nama}
{item.nama} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5}>
{new Date(item.tanggal).toLocaleDateString('id-ID', { {new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5}>
{item.jenisKelamin} {item.jenisKelamin}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" lh={1.5} c="dimmed" truncate="end" lineClamp={1}>
<Text truncate fz="sm" c="dimmed"> {item.alamat}
{item.alamat} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -157,9 +147,10 @@ function ListKelahiran({ search }: { search: string }) {
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}` `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`
) )
} }
size="compact-sm"
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={18} />
<Text ml={5}>Detail</Text> <Text ml="xs">Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -167,8 +158,8 @@ function ListKelahiran({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py="lg">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kelahiran yang cocok Tidak ada data kelahiran yang cocok
</Text> </Text>
</Center> </Center>
@@ -178,27 +169,83 @@ function ListKelahiran({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack hiddenFrom="md" gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} p="md" withBorder radius="sm">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.nama}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Tanggal</Text>
<Text fz="sm" fw={500} lh={1.5}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Jenis Kelamin</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.jenisKelamin}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Alamat</Text>
<Text fz="sm" fw={500} lh={1.5} c="dimmed">{item.alamat}</Text>
</Box>
<Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`
)
}
size="sm"
>
<IconDeviceImacCog size={18} />
<Text ml="xs">Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="lg">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kelahiran yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
<Center> {totalPages > 1 && (
<Pagination <Center>
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" mt="md"
color="blue" mb="md"
radius="md" color="blue"
/> radius="md"
</Center> />
</Box> </Center>
)}
</Stack>
); );
} }
export default Kelahiran; export default Kelahiran;

View File

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

View File

@@ -48,7 +48,7 @@ function DetailKematian() {
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol kembali */} {/* Tombol kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -62,7 +62,7 @@ function DetailKematian() {
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "50%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

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

View File

@@ -20,28 +20,25 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Kematian() { function Kematian() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter(); const router = useRouter();
return ( return (
<Box> <Box>
{/* Tombol Back */} {/* Tombol Back */}
<Box mb={10}> <Box mb="md">
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian')}> <Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian')}>
<IconArrowBack color={colors["blue-button"]} size={30} /> <IconArrowBack color={colors["blue-button"]} size={30} />
</Button> </Button>
</Box> </Box>
{/* Header dengan Search */} {/* Header dengan Search */}
<HeaderSearch <HeaderSearch
title='Data Kematian' title='Data Kematian'
@@ -51,43 +48,38 @@ function Kematian() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKematian search={search} /> <ListKematian search={search} />
</Box> </Box>
); );
} }
function ListKematian({ search }: { search: string }) { function ListKematian({ search }: { search: string }) {
const statePersentase = useProxy(persentasekelahiran.kematian); const statePersentase = useProxy(persentasekelahiran.kematian);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = statePersentase.findMany; const { data, page, totalPages, loading, load } = statePersentase.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Data Kematian</Title> <Title order={2} visibleFrom="md">Daftar Data Kematian</Title>
<Title order={3} hiddenFrom="md">Daftar Data Kematian</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -102,16 +94,16 @@ function ListKematian({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Tabel untuk desktop */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.2}>Nama</Text></TableTh>
<TableTh>Tanggal</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.2}>Tanggal</Text></TableTh>
<TableTh>Jenis Kelamin</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.2}>Jenis Kelamin</Text></TableTh>
<TableTh>Alamat</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.2}>Alamat</Text></TableTh>
<TableTh>Aksi</TableTh> <TableTh><Text fz="sm" fw={600} lh={1.2}>Aksi</Text></TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -119,45 +111,39 @@ function ListKematian({ search }: { search: string }) {
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.nama}
{item.nama} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5}>
{new Date(item.tanggal).toLocaleDateString('id-ID', { {new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5}>{item.jenisKelamin}</Text>
{item.jenisKelamin}
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5} c="dimmed" truncate="end">
<Text truncate fz="sm" c="dimmed"> {item.alamat}
{item.alamat} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => onClick={() =>
router.push( router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${item.id}` `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${item.id}`
) )
} }
> >
<IconEdit size={18} /> Detail
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -165,8 +151,8 @@ function ListKematian({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kematian yang cocok Tidak ada data kematian yang cocok
</Text> </Text>
</Center> </Center>
@@ -176,27 +162,80 @@ function ListKematian({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Card untuk mobile */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap="xs">
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.45}>{item.nama}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Tanggal</Text>
<Text fz="sm" fw={500} lh={1.45}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Jenis Kelamin</Text>
<Text fz="sm" fw={500} lh={1.45}>{item.jenisKelamin}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Alamat</Text>
<Text fz="sm" fw={500} lh={1.45} c="dimmed">{item.alamat}</Text>
</Box>
<Box mt="xs">
<Button
fullWidth
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${item.id}`
)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kematian yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
<Center> {totalPages > 1 && (
<Pagination <Center mt="lg">
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10, search); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" color="blue"
color="blue" radius="md"
radius="md" />
/> </Center>
</Center> )}
</Box> </Box>
); );
} }
export default Kematian; export default Kematian;

View File

@@ -3,275 +3,317 @@
'use client' 'use client'
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Badge, Box, Center, Flex, Tooltip as MantineTooltip, Paper, Select, Skeleton, Stack, Table, Text, Title } from '@mantine/core'; import { ActionIcon, Badge, Box, Center, Flex, Group, Paper, Select, Skeleton, Stack, Table, Text, Title, Tooltip as MantineTooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconBabyCarriage, IconGrave2 } from '@tabler/icons-react'; import { IconBabyCarriage, IconGrave2 } 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 { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip as RechartsTooltip, TooltipProps, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
type TooltipPayload = { type TooltipPayload = {
name: string; name: string;
value: number; value: number;
payload: any; payload: any;
color: string; color: string;
dataKey: string; dataKey: string;
}; };
type CustomTooltipProps = TooltipProps<number, string> & { type CustomTooltipProps = TooltipProps<number, string> & {
active?: boolean; active?: boolean;
payload?: TooltipPayload[]; payload?: TooltipPayload[];
label?: string; label?: string;
}; };
function PersentaseDataKelahiranKematian() { function PersentaseDataKelahiranKematian() {
return ( return (
<Stack gap="md"> <Stack gap="md">
<GrafikPersentaseKelahiranKematian /> <GrafikPersentaseKelahiranKematian />
</Stack> </Stack>
); );
} }
function GrafikPersentaseKelahiranKematian() { function GrafikPersentaseKelahiranKematian() {
const router = useRouter(); const router = useRouter();
type DataTahunan = {
tahun: string;
totalKelahiran: number;
totalKematian: number;
data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
type DataTahunan = { // ✅ Fungsi hitung tahunan + bulanan
tahun: string; const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => {
totalKelahiran: number; const dataTahunan: Record<string, DataTahunan> = {};
totalKematian: number;
data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
const namaBulan = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
];
// ✅ Fungsi hitung tahunan + bulanan kelahiran?.forEach((item: any) => {
const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => { const date = new Date(item.tanggal);
const dataTahunan: Record<string, DataTahunan> = {}; const tahun = date.getFullYear().toString();
const bulanIndex = date.getMonth();
if (!dataTahunan[tahun]) {
dataTahunan[tahun] = {
tahun,
totalKelahiran: 0,
totalKematian: 0,
data: namaBulan.map((nama, idx) => ({
id: `${tahun}-${idx + 1}`,
bulan: nama,
kelahiran: 0,
kematian: 0
}))
};
}
const namaBulan = [ dataTahunan[tahun].totalKelahiran += 1;
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', dataTahunan[tahun].data[bulanIndex].kelahiran += 1;
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' });
];
kematian?.forEach((item: any) => {
const date = new Date(item.tanggal);
const tahun = date.getFullYear().toString();
const bulanIndex = date.getMonth();
// Proses kelahiran if (!dataTahunan[tahun]) {
kelahiran?.forEach((item: any) => { dataTahunan[tahun] = {
const date = new Date(item.tanggal); tahun,
const tahun = date.getFullYear().toString(); totalKelahiran: 0,
const bulanIndex = date.getMonth(); totalKematian: 0,
data: namaBulan.map((nama, idx) => ({
id: `${tahun}-${idx + 1}`,
bulan: nama,
kelahiran: 0,
kematian: 0
}))
};
}
dataTahunan[tahun].totalKematian += 1;
dataTahunan[tahun].data[bulanIndex].kematian += 1;
});
if (!dataTahunan[tahun]) { return Object.values(dataTahunan).sort((a, b) => parseInt(a.tahun) - parseInt(b.tahun));
dataTahunan[tahun] = { };
tahun,
totalKelahiran: 0,
totalKematian: 0,
data: namaBulan.map((nama, idx) => ({
id: `${tahun}-${idx + 1}`,
bulan: nama,
kelahiran: 0,
kematian: 0
}))
};
}
const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<DataTahunan[]>([]);
const [selectedYear, setSelectedYear] = useState<string | null>(null);
dataTahunan[tahun].totalKelahiran += 1; const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num);
dataTahunan[tahun].data[bulanIndex].kelahiran += 1;
});
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
if (active && payload && payload.length) {
return (
<Paper p="sm" shadow="md" withBorder radius="md">
<Text fz="sm" fw={600} lh={1.4}>Tahun {label}</Text>
<Text fz="sm" c="blue.6" lh={1.4}>Kelahiran: {formatNumber(payload[0].value)}</Text>
<Text fz="sm" c="red.6" lh={1.4}>Kematian: {formatNumber(payload[1].value)}</Text>
</Paper>
);
}
return null;
};
// Proses kematian useShallowEffect(() => {
kematian?.forEach((item: any) => { statePersentase.kelahiran.findMany.load(1, 1000);
const date = new Date(item.tanggal); statePersentase.kematian.findMany.load(1, 1000);
const tahun = date.getFullYear().toString(); }, []);
const bulanIndex = date.getMonth();
useEffect(() => {
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
const hasil = countByYearAndMonth(
statePersentase.kelahiran.findMany.data,
statePersentase.kematian.findMany.data
);
if (!dataTahunan[tahun]) { setChartData(hasil);
dataTahunan[tahun] = { setSelectedYear(hasil[0]?.tahun || null);
tahun, }
totalKelahiran: 0, }, [statePersentase.kelahiran.findMany.data, statePersentase.kematian.findMany.data]);
totalKematian: 0,
data: namaBulan.map((nama, idx) => ({
id: `${tahun}-${idx + 1}`,
bulan: nama,
kelahiran: 0,
kematian: 0
}))
};
}
if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
return <Skeleton h={400} radius="lg" />;
}
dataTahunan[tahun].totalKematian += 1; const selectedYearData = chartData.find(d => d.tahun === selectedYear);
dataTahunan[tahun].data[bulanIndex].kematian += 1;
});
return (
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Stack gap='md'>
<Group justify="space-between" align="center">
<Title order={3} fw={700} fz={{ base: 'sm', md: 'md' }} lh={1.2}>
Statistik Kelahiran & Kematian
</Title>
<Flex gap="sm">
<MantineTooltip label="Tambah Data Kelahiran" withArrow>
<ActionIcon size="lg" radius="xl" color="blue.6" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}>
<IconBabyCarriage size={22} />
</ActionIcon>
</MantineTooltip>
<MantineTooltip label="Tambah Data Kematian" withArrow>
<ActionIcon size="lg" radius="xl" color="red.6" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')}>
<IconGrave2 size={22} />
</ActionIcon>
</MantineTooltip>
</Flex>
</Group>
return Object.values(dataTahunan).sort((a, b) => parseInt(a.tahun) - parseInt(b.tahun)); {chartData.length === 0 ? (
}; <Center py={{ base: 'xl', md: '2xl' }}>
<Text c="dimmed" fs="italic" fz="sm" lh={1.4}>
Belum ada data untuk ditampilkan
</Text>
</Center>
) : (
<>
<Box maw={220}>
<Select
label="Pilih Tahun"
placeholder="Pilih tahun data"
data={chartData.map((item) => ({ value: item.tahun, label: item.tahun }))}
value={selectedYear}
onChange={(value) => setSelectedYear(value || null)}
size="sm"
radius="md"
/>
</Box>
<Box h={360}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 0, bottom: 10 }}>
<XAxis dataKey="tahun" />
<YAxis />
<RechartsTooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="totalKelahiran" name="Kelahiran" fill="#4dabf7" radius={[6, 6, 0, 0]} />
<Bar dataKey="totalKematian" name="Kematian" fill="#f03e3e" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Box>
const statePersentase = useProxy(persentasekelahiran); {selectedYearData && (
const [chartData, setChartData] = useState<DataTahunan[]>([]); <Stack gap="md">
const [selectedYear, setSelectedYear] = useState<string | null>(null); <Flex align="center" gap="sm">
<Title order={4} fw={600} fz={{ base: 'xs', md: 'sm' }} lh={1.2}>
Rincian Tahun {selectedYear}
</Title>
<Badge variant="light" color="blue" fz={{ base: 'xs', md: 'sm' }}>
{formatNumber(selectedYearData.totalKelahiran)} kelahiran
</Badge>
<Badge variant="light" color="red" fz={{ base: 'xs', md: 'sm' }}>
{formatNumber(selectedYearData.totalKematian)} kematian
</Badge>
</Flex>
{/* Desktop: Table */}
<Box visibleFrom="md">
<Table striped withTableBorder highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th fz="sm" fw={600} lh={1.4}>Bulan</Table.Th>
<Table.Th ta="right" fz="sm" fw={600} lh={1.4}>Kelahiran</Table.Th>
<Table.Th ta="right" fz="sm" fw={600} lh={1.4}>Kematian</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{selectedYearData.data.length > 0 ? (
<>
{selectedYearData.data.map((item) => (
<Table.Tr key={item.id}>
<Table.Td fz="sm" fw={500} lh={1.5}>{item.bulan}</Table.Td>
<Table.Td ta="right" fz="sm" fw={500} lh={1.5}>
{formatNumber(item.kelahiran)}
</Table.Td>
<Table.Td ta="right" fz="sm" fw={500} lh={1.5}>
{formatNumber(item.kematian)}
</Table.Td>
</Table.Tr>
))}
<Table.Tr>
<Table.Td fz="sm" fw={600} lh={1.5}>Total</Table.Td>
<Table.Td ta="right" fz="sm" fw={600} lh={1.5}>
{formatNumber(selectedYearData.totalKelahiran)}
</Table.Td>
<Table.Td ta="right" fz="sm" fw={600} lh={1.5}>
{formatNumber(selectedYearData.totalKematian)}
</Table.Td>
</Table.Tr>
</>
) : (
<Table.Tr>
<Table.Td colSpan={3} ta="center" c="dimmed" fz="sm" lh={1.4}>
Tidak ada rincian bulanan
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Box>
const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num); {/* Mobile: Card List */}
<Box hiddenFrom="md">
<Stack gap="xs">
{selectedYearData.data.length > 0 ? (
selectedYearData.data.map((item) => (
<Paper key={item.id} p="sm" radius="md" withBorder>
<Text fz="xs" fw={600} lh={1.4}>Bulan</Text>
<Text fz="sm" fw={500} lh={1.4} mb="sm">
{item.bulan}
</Text>
<Text fz="xs" fw={600} lh={1.4}>Kelahiran</Text>
<Text fz="sm" fw={500} lh={1.4} mb="sm">
{formatNumber(item.kelahiran)}
</Text>
<Text fz="xs" fw={600} lh={1.4}>Kematian</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatNumber(item.kematian)}
</Text>
</Paper>
))
) : (
<Center py="md">
<Text c="dimmed" fz="sm" lh={1.4}>Tidak ada rincian bulanan</Text>
</Center>
)}
{/* Total row mobile */}
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { {selectedYearData.data.length > 0 && (
if (active && payload && payload.length) { <Paper p="sm" radius="md" withBorder bg="gray.1">
return ( <Text fz="xs" fw={600} lh={1.4}>Total</Text>
<Paper p="sm" shadow="md" withBorder radius="md"> <Flex justify="space-between" mt="xs">
<Text size="sm" fw={600}>Tahun {label}</Text> <Text fz="sm" fw={600} lh={1.4}>Kelahiran</Text>
<Text size="sm" c="blue.6">Kelahiran: {formatNumber(payload[0].value)}</Text> <Text fz="sm" fw={600} lh={1.4}>
<Text size="sm" c="red.6">Kematian: {formatNumber(payload[1].value)}</Text> {formatNumber(selectedYearData.totalKelahiran)}
</Paper> </Text>
); </Flex>
} <Flex justify="space-between" mt="xs">
return null; <Text fz="sm" fw={600} lh={1.4}>Kematian</Text>
}; <Text fz="sm" fw={600} lh={1.4}>
{formatNumber(selectedYearData.totalKematian)}
</Text>
useShallowEffect(() => { </Flex>
statePersentase.kelahiran.findMany.load(1, 1000); </Paper>
statePersentase.kematian.findMany.load(1, 1000); )}
}, []); </Stack>
</Box>
</Stack>
useEffect(() => { )}
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) { </>
const hasil = countByYearAndMonth( )}
statePersentase.kelahiran.findMany.data, </Stack>
statePersentase.kematian.findMany.data </Paper>
); );
setChartData(hasil);
setSelectedYear(hasil[0]?.tahun || null);
}
}, [statePersentase.kelahiran.findMany.data, statePersentase.kematian.findMany.data]);
if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
return <Skeleton h={400} radius="lg" />;
}
const selectedYearData = chartData.find(d => d.tahun === selectedYear);
return (
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack gap="lg">
<Flex justify="space-between" align="center">
<Title order={3} fw={700}>Statistik Kelahiran & Kematian</Title>
<Flex gap="sm">
<MantineTooltip label="Tambah Data Kelahiran" withArrow>
<ActionIcon size="lg" radius="xl" color="blue.6" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}>
<IconBabyCarriage size={22} />
</ActionIcon>
</MantineTooltip>
<MantineTooltip label="Tambah Data Kematian" withArrow>
<ActionIcon size="lg" radius="xl" color="red.6" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')}>
<IconGrave2 size={22} />
</ActionIcon>
</MantineTooltip>
</Flex>
</Flex>
{chartData.length === 0 ? (
<Center py="xl">
<Text c="dimmed" fs="italic">Belum ada data untuk ditampilkan</Text>
</Center>
) : (
<>
<Box maw={220}>
<Select
label="Pilih Tahun"
placeholder="Pilih tahun data"
data={chartData.map((item) => ({ value: item.tahun, label: item.tahun }))}
value={selectedYear}
onChange={(value) => setSelectedYear(value || null)}
size="sm"
radius="md"
/>
</Box>
<Box h={360}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 0, bottom: 10 }}>
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="totalKelahiran" name="Kelahiran" fill="#4dabf7" radius={[6, 6, 0, 0]} />
<Bar dataKey="totalKematian" name="Kematian" fill="#f03e3e" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Box>
{selectedYearData && (
<Box>
<Flex align="center" gap="sm" mb="md">
<Title order={4} fw={600}>Rincian Tahun {selectedYear}</Title>
<Badge variant="light" color="blue">{formatNumber(selectedYearData.totalKelahiran)} kelahiran</Badge>
<Badge variant="light" color="red">{formatNumber(selectedYearData.totalKematian)} kematian</Badge>
</Flex>
<Table striped withTableBorder highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Bulan</Table.Th>
<Table.Th ta="right">Kelahiran</Table.Th>
<Table.Th ta="right">Kematian</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{selectedYearData.data.length > 0 ? (
<>
{selectedYearData.data.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.bulan}</Table.Td>
<Table.Td ta="right">{formatNumber(item.kelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(item.kematian)}</Table.Td>
</Table.Tr>
))}
<Table.Tr style={{ fontWeight: 'bold' }}>
<Table.Td>Total</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKematian)}</Table.Td>
</Table.Tr>
</>
) : (
<Table.Tr>
<Table.Td colSpan={3} ta="center" c="dimmed">Tidak ada rincian bulanan</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Box>
)}
</>
)}
</Stack>
</Paper>
);
} }
export default PersentaseDataKelahiranKematian; export default PersentaseDataKelahiranKematian;

View File

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

View File

@@ -49,7 +49,7 @@ function DetailInfoWabahPenyakit() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Back */} {/* Tombol Back */}
<Button <Button
variant="subtle" variant="subtle"
@@ -63,7 +63,7 @@ function DetailInfoWabahPenyakit() {
{/* Wrapper Detail */} {/* Wrapper Detail */}
<Paper <Paper
withBorder withBorder
w={{ base: '100%', md: '50%' }} w={{ base: '100%', md: '70%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -89,6 +89,7 @@ function DetailInfoWabahPenyakit() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi Lengkap</Text> <Text fz="lg" fw="bold">Deskripsi Lengkap</Text>
<Text <Text
pl={10}
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap }}

View File

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

View File

@@ -16,9 +16,9 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { 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';
@@ -27,7 +27,7 @@ import HeaderSearch from '../../_com/header';
import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
function InfoWabahPenyakit() { function InfoWabahPenyakit() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
{/* Header Search */} {/* Header Search */}
@@ -44,8 +44,9 @@ function InfoWabahPenyakit() {
} }
function ListInfoWabahPenyakit({ search }: { search: string }) { function ListInfoWabahPenyakit({ search }: { search: string }) {
const infoWabahPenyakitState = useProxy(infoWabahPenyakit) const infoWabahPenyakitState = useProxy(infoWabahPenyakit);
const router = useRouter() const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -56,25 +57,30 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
} = infoWabahPenyakitState.findMany; } = infoWabahPenyakitState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch);
}, [page, search]) }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Info Wabah Penyakit</Title> <Title
order={4}
lh={{ base: 1.2, md: 1.1 }}
>
Daftar Info Wabah Penyakit
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -85,8 +91,8 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
{/* Tabel */} {/* Desktop Table */}
<Box style={{ overflowX: "auto" }}> <Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
@@ -100,16 +106,19 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> fz="sm"
</Box> c="dimmed"
lh={1.45}
truncate="end"
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
/>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -118,16 +127,18 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)} onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)}
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.45}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data info wabah penyakit yang cocok Tidak ada data info wabah penyakit yang cocok
</Text> </Text>
</Center> </Center>
@@ -137,6 +148,53 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card List */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap={"xs"}>
<Text fz="sm" fw={600} lh={1.4}>
Judul
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.name}
</Text>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Text
fz="sm"
fw={500}
lh={1.45}
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
/>
<Button
variant="light"
color="blue"
fullWidth
mt="xs"
onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={6} fz="sm" fw={500} lh={1.45}>
Detail
</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data info wabah penyakit yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -144,8 +202,8 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10) load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
@@ -155,7 +213,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default InfoWabahPenyakit; export default InfoWabahPenyakit;

View File

@@ -128,7 +128,7 @@ function EditKontakDarurat() {
if (loading) return <Text>Loading...</Text>; if (loading) return <Text>Loading...</Text>;
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} />

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