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 */
'use client'
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 { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -72,35 +72,76 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
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>
))}
</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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ function DetailFoto() {
const imageUrl = data.imageGalleryFoto?.link;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -59,7 +59,7 @@ function DetailFoto() {
withBorder
// Gunakan max-width agar tidak terlalu lebar di desktop
maw={800}
w="100%"
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,8 +41,7 @@ function PerizinanBerusaha() {
const loadData = async () => {
try {
setLoading(true);
// You should get the ID from your router query or params
const id = 'edit'; // Replace with actual ID or get from URL params
const id = 'edit';
await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) {
setError('Gagal memuat data');
@@ -66,7 +65,7 @@ function PerizinanBerusaha() {
if (error || !pelayananPerizinanBerusaha.findById.data) {
return (
<Center h={200}>
<Text>{error || 'Data tidak ditemukan'}</Text>
<Text c="dimmed">{error || 'Data tidak ditemukan'}</Text>
</Center>
);
}
@@ -79,24 +78,24 @@ function PerizinanBerusaha() {
{/* Header */}
<Grid align="center">
<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
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
)
}
>
Edit
</Button>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
)
}
>
Edit
</Button>
</GridCol>
</Grid>
@@ -104,38 +103,40 @@ function PerizinanBerusaha() {
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 0, md: 50 }} pb="xl">
<Center>
<Text
<Title
order={3}
ta="center"
fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']}
lh={1.15}
>
{data.name}
</Text>
</Title>
</Center>
<Divider my="md" color={colors['blue-button']} />
<Box mt="lg">
<Text
py={10}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
py="xs"
ta={{ base: "left", md: "justify" }}
fz={{ base: 'sm', md: 'md' }}
lh={1.55}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
<Text
py={10}
fz={{ base: '1rem', md: '1.2rem' }}
fw="bold"
py="xs"
fz={{ base: 'sm', md: 'md' }}
fw={700}
c={colors['blue-button']}
lh={1.5}
>
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
umum:
</Text>
<Box p="xl" w="100%">
<Box p="xl" w="100%" visibleFrom='md'>
<Stepper
active={active}
onStepClick={setActive}
@@ -143,28 +144,115 @@ function PerizinanBerusaha() {
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">
Pendaftaran akun pada portal OSS
<Text fz="sm" lh={1.5}>
Pendaftaran akun pada portal OSS
</Text>
</StepperStep>
<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 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 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 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 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>
<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>
</Stepper>
@@ -177,9 +265,10 @@ function PerizinanBerusaha() {
</Box>
<Text
py={35}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
py="md"
ta={{ base: "left", md: "justify" }}
fz={{ base: 'sm', md: 'md' }}
lh={1.55}
>
Penting untuk diingat bahwa prosedur dan persyaratan dapat
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 (
<Box>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Text fw="bold" fz="sm" mb={6}>
{title}
</Text>

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -25,9 +25,10 @@ import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useDebouncedValue } from '@mantine/hooks';
function SuratKeterangan() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
@@ -45,6 +46,7 @@ function SuratKeterangan() {
function ListSuratKeterangan({ search }: { search: string }) {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -55,72 +57,80 @@ function ListSuratKeterangan({ search }: { search: string }) {
} = suratKeteranganState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = useMemo(() => {
if (!data) return [];
const keyword = search.toLowerCase();
return data.filter(item =>
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
const keyword = debouncedSearch.toLowerCase();
return data.filter(
(item) =>
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
}, [data, search]);
}, [data, debouncedSearch]);
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Surat Keterangan</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
}
>
Tambah Baru
</Button>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={1.2}>
List Surat Keterangan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh fz="sm" fw={600} ta="left">
Nama
</TableTh>
<TableTh fz="sm" fw={600} ta="left">
Deskripsi
</TableTh>
<TableTh fz="sm" fw={600} ta="left">
Aksi
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Box>
<TableTd>
<Text
fz="sm"
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '' }}
style={{ wordBreak: 'break-word' }}
/>
</TableTd>
<TableTd style={{ width: '15%' }}>
<TableTd>
<Button
size="xs"
radius="md"
@@ -128,7 +138,9 @@ function ListSuratKeterangan({ search }: { search: string }) {
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
router.push(
`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`
)
}
>
Detail
@@ -139,8 +151,10 @@ function ListSuratKeterangan({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text>
<Center py="xl">
<Text c="dimmed" fz="sm" ta="center">
Tidak ada data surat keterangan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -148,7 +162,67 @@ function ListSuratKeterangan({ search }: { search: string }) {
</TableTbody>
</Table>
</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>
<Center>
<Pagination
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 = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
link: originalData.link,
});
toast.info("Form dikembalikan ke data awal");
};
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
link: originalData.link,
});
toast.info("Form dikembalikan ke data awal");
};
// Submit: update global state hanya saat simpan
const handleSubmit = async () => {
@@ -102,12 +102,12 @@ function EditPelayananTelunjukSakti() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Back Button + Title */}
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Pelayanan Telunjuk Sakti Desa
</Title>

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ function CreatePenghargaan() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="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 { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import { useDebouncedValue } from '@mantine/hooks';
function Penghargaan() {
const [search, setSearch] = useState("");
@@ -45,45 +46,48 @@ function Penghargaan() {
function ListPenghargaan({ search }: { search: string }) {
const state = useProxy(penghargaanState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || []
const filteredData = data || [];
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
<Stack py="md">
<Skeleton h={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Box py="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>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/penghargaan/create')}
>
Tambah Baru
</Button>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/penghargaan/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '30%' }}>Aksi</TableTh>
<TableTh w="35%">Nama</TableTh>
<TableTh w="35%">Deskripsi</TableTh>
<TableTh w="30%">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -91,31 +95,27 @@ function ListPenghargaan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box w={200}>
<Text
truncate="end"
lineClamp={1}
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Box>
<Text
fz="sm"
lh={1.45}
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
lineClamp={1}
/>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`)
}
@@ -127,9 +127,9 @@ function ListPenghargaan({ search }: { search: string }) {
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
<TableTd colSpan={3}>
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data penghargaan yang cocok
</Text>
</Center>
@@ -139,7 +139,54 @@ function ListPenghargaan({ search }: { search: string }) {
</TableTbody>
</Table>
</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>
<Center>
<Pagination
value={page}
@@ -148,7 +195,7 @@ function ListPenghargaan({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mt="lg"
mb="md"
color="blue"
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 */
'use client'
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 { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -54,35 +54,76 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,7 +79,7 @@ function CreatePotensi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="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 HeaderSearch from '../../../_com/header';
import potensiDesaState from '../../../_state/desa/potensi';
import { useDebouncedValue } from '@mantine/hooks';
function Potensi() {
const [search, setSearch] = useState("");
@@ -46,6 +47,7 @@ function Potensi() {
function ListPotensi({ search }: { search: string }) {
const potensiState = useProxy(potensiDesaState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -57,41 +59,61 @@ function ListPotensi({ search }: { search: string }) {
useEffect(() => {
potensiState.kategoriPotensi.findMany.load();
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Potensi Desa</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
>
Tambah Baru
</Button>
<Box py="lg">
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={1.2}>
Daftar Potensi Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover style={{ minWidth: '700px' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={700}>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Detail</TableTh>
<TableTh w="20%">
<Text fz="sm" fw={600} lh={1.4}>
Judul
</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>
</TableThead>
<TableTbody>
@@ -99,27 +121,23 @@ function ListPotensi({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box w={200}>
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
</Box>
<Text fz="sm" c="gray.7" lh={1.5}>
{item.kategori?.nama || '-'}
</Text>
</TableTd>
<TableTd>
<Box w={300}>
<Text
lineClamp={1}
truncate
fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
<Text
fz="sm"
lh={1.5}
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{ wordBreak: 'break-word' }}
/>
</TableTd>
<TableTd>
<Button
@@ -138,8 +156,10 @@ function ListPotensi({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data potensi yang cocok</Text>
<Center py="xl">
<Text c="gray.6" fz="sm" ta="center" lh={1.5}>
Tidak ada data potensi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -147,7 +167,64 @@ function ListPotensi({ search }: { search: string }) {
</TableTbody>
</Table>
</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>
<Center>
<Pagination
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 */
'use client'
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 { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -60,35 +60,76 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<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

View File

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

View File

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

View File

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

View File

@@ -126,7 +126,7 @@ function Page() {
// ⏳ Loading
if (isLoading) {
return (
<Box>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Center h={400}>
<Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} />
@@ -142,7 +142,7 @@ function Page() {
// ❌ Error
if (loadError) {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -1,7 +1,7 @@
'use client'
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 { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -30,208 +30,558 @@ function Page() {
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
{/* 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">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<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']} />
<Text fz={{ base: "md", md: "h3" }} style={{wordBreak: "break-word", whiteSpace: "normal"}} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
</Paper>
</Box>
</Paper>
)}
<Box visibleFrom='md'>
{sejarah && (
<Paper p={{ base: "lg", md: "xl" }} bg={'white'} withBorder radius="md" shadow="xs">
<Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
<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>
</Group>
<Box py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<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 */}
{visiMisi && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Box visibleFrom='md'>
{visiMisi && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Visi Misi 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/${visiMisi.id}/visi_misi_desa`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
>
Edit
</Button>
</Group>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<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" }}>
Visi Misi Desa
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<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.visi }} />
<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 }} />
</Paper>
</Box>
</Paper>
)}
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<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" }}>
Visi Misi Desa
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<Box px={20}>
<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.visi }} />
</Box>
<Box px={20}>
<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 && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Box visibleFrom='md'>
{lambang && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Lambang 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/${lambang.id}/lambang_desa`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
>
Edit
</Button>
</Group>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<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" }}>
{lambang.judul}
</Text>
</Paper>
</Stack>
<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 }} />
</Paper>
</Box>
</Paper>
)}
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center>
<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" }}>
{lambang.judul}
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<Box px={20}>
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
</Box>
</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 && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Box visibleFrom='md'>
{maskot && (
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Group justify='space-between'>
<Title order={3} c={colors['blue-button']}>Preview Maskot 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/${maskot.id}/maskot_desa`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
>
Edit
</Button>
</Group>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
</Center>
<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" }}>
Maskot Desa
</Text>
</Paper>
</Stack>
<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 }} />
<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 px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
</Center>
<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" }}>
Maskot Desa
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<Box px={20}>
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} 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>
<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>
</Paper>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,18 +33,18 @@ function Page() {
{/* Header + tombol edit */}
<Grid align="center">
<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 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-perbekel/${perbekel.id}`)}
>
Edit
</Button>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
@@ -58,7 +58,13 @@ function Page() {
</Center>
</GridCol>
<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
</Text>
</GridCol>
@@ -86,25 +92,55 @@ function Page() {
className="glass3"
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" }}>
I.B. Surya Prabhawa Manuaba, S.H., M.H.
<Text
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>
</Paper>
</Stack>
{/* Biodata & Info */}
<Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
<Title order={3} lh={1.2} mb={4} />
<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>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
<Title order={3} lh={1.2} mt="md" mb={4} />
<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>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
<Title order={3} lh={1.2} mt="md" mb={4} />
<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>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
<Title order={3} lh={1.2} mt="md" mb={4} />
<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>
</Paper>
</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 */
'use client'
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 { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -81,52 +81,93 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
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
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
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>
{children}
</TabsPanel>
))}
</Tabs>
</Stack >
);
}

View File

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

View File

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

View File

@@ -94,7 +94,7 @@ function CreateArtikelKesehatan() {
};
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 */}
<Group mb="md">
<Button

View File

@@ -18,7 +18,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -50,10 +50,11 @@ function ListArtikelKesehatan({ search }: { search: string }) {
const router = useRouter();
const { data, page, totalPages, loading, load } = stateArtikel.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];

View File

@@ -131,7 +131,7 @@ function EditFasilitasKesehatan() {
};
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 */}
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

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

View File

@@ -70,7 +70,7 @@ function CreateFasilitasKesehatan() {
}, []);
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 */}
<Group mb="md">
<Button

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ function CreateDokter() {
}
};
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 */}
<Group mb="md">
<Button

View File

@@ -1,7 +1,7 @@
'use client'
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 { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
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 { useState } from 'react';
function DokterTenagaMedis() {
const [search, setSearch] = useState("");
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Box mb="sm">
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
@@ -44,25 +43,29 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
totalPages
} = stateFasilitasKesehatan.findMany
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Box py={10}>
<Box py="md">
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Judul + Tombol Tambah */}
<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
leftSection={<IconPlus size={18} />}
color="blue"
@@ -77,15 +80,15 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
</Button>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Dokter</TableTh>
<TableTh>Spesialis</TableTh>
<TableTh>Jadwal</TableTh>
<TableTh>Aksi</TableTh>
<TableTh><Text fz="sm" fw={600} lh={1.4}>Nama Dokter</Text></TableTh>
<TableTh><Text fz="sm" fw={600} lh={1.4}>Spesialis</Text></TableTh>
<TableTh><Text fz="sm" fw={600} lh={1.4}>Jadwal</Text></TableTh>
<TableTh><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -93,21 +96,17 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box w={150}>
<Text fz="md" fw={500} lh={1.5}>
{item.specialist || '-'}
</Box>
</Text>
</TableTd>
<TableTd>
<Box w={150}>
<Text dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
</Box>
<Text fz="md" fw={500} lh={1.5} dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
</TableTd>
<TableTd>
<Button
@@ -120,7 +119,7 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
<Text ml="xs" fz="sm" fw={500}>Detail</Text>
</Button>
</TableTd>
</TableTr>
@@ -128,8 +127,8 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
@@ -139,6 +138,47 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
</TableTbody>
</Table>
</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>
{/* Pagination */}
@@ -160,4 +200,4 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
)
}
export default DokterTenagaMedis;
export default DokterTenagaMedis;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ function CreateJadwalKegiatan() {
};
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 */}
<Group mb="md">
<Button

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,275 +3,317 @@
'use client'
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
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 { IconBabyCarriage, IconGrave2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
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';
type TooltipPayload = {
name: string;
value: number;
payload: any;
color: string;
dataKey: string;
name: string;
value: number;
payload: any;
color: string;
dataKey: string;
};
type CustomTooltipProps = TooltipProps<number, string> & {
active?: boolean;
payload?: TooltipPayload[];
label?: string;
active?: boolean;
payload?: TooltipPayload[];
label?: string;
};
function PersentaseDataKelahiranKematian() {
return (
<Stack gap="md">
<GrafikPersentaseKelahiranKematian />
</Stack>
);
return (
<Stack gap="md">
<GrafikPersentaseKelahiranKematian />
</Stack>
);
}
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 = {
tahun: string;
totalKelahiran: number;
totalKematian: number;
data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
// ✅ Fungsi hitung tahunan + bulanan
const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => {
const dataTahunan: Record<string, DataTahunan> = {};
const namaBulan = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
];
// ✅ Fungsi hitung tahunan + bulanan
const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => {
const dataTahunan: Record<string, DataTahunan> = {};
kelahiran?.forEach((item: any) => {
const date = new Date(item.tanggal);
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 = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
];
dataTahunan[tahun].totalKelahiran += 1;
dataTahunan[tahun].data[bulanIndex].kelahiran += 1;
});
kematian?.forEach((item: any) => {
const date = new Date(item.tanggal);
const tahun = date.getFullYear().toString();
const bulanIndex = date.getMonth();
// Proses kelahiran
kelahiran?.forEach((item: any) => {
const date = new Date(item.tanggal);
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
}))
};
}
dataTahunan[tahun].totalKematian += 1;
dataTahunan[tahun].data[bulanIndex].kematian += 1;
});
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
}))
};
}
return Object.values(dataTahunan).sort((a, b) => parseInt(a.tahun) - parseInt(b.tahun));
};
const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<DataTahunan[]>([]);
const [selectedYear, setSelectedYear] = useState<string | null>(null);
dataTahunan[tahun].totalKelahiran += 1;
dataTahunan[tahun].data[bulanIndex].kelahiran += 1;
});
const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num);
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
kematian?.forEach((item: any) => {
const date = new Date(item.tanggal);
const tahun = date.getFullYear().toString();
const bulanIndex = date.getMonth();
useShallowEffect(() => {
statePersentase.kelahiran.findMany.load(1, 1000);
statePersentase.kematian.findMany.load(1, 1000);
}, []);
useEffect(() => {
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
const hasil = countByYearAndMonth(
statePersentase.kelahiran.findMany.data,
statePersentase.kematian.findMany.data
);
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
}))
};
}
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" />;
}
dataTahunan[tahun].totalKematian += 1;
dataTahunan[tahun].data[bulanIndex].kematian += 1;
});
const selectedYearData = chartData.find(d => d.tahun === selectedYear);
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);
const [chartData, setChartData] = useState<DataTahunan[]>([]);
const [selectedYear, setSelectedYear] = useState<string | null>(null);
{selectedYearData && (
<Stack gap="md">
<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>
)}
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
if (active && payload && payload.length) {
return (
<Paper p="sm" shadow="md" withBorder radius="md">
<Text size="sm" fw={600}>Tahun {label}</Text>
<Text size="sm" c="blue.6">Kelahiran: {formatNumber(payload[0].value)}</Text>
<Text size="sm" c="red.6">Kematian: {formatNumber(payload[1].value)}</Text>
</Paper>
);
}
return null;
};
useShallowEffect(() => {
statePersentase.kelahiran.findMany.load(1, 1000);
statePersentase.kematian.findMany.load(1, 1000);
}, []);
useEffect(() => {
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
const hasil = countByYearAndMonth(
statePersentase.kelahiran.findMany.data,
statePersentase.kematian.findMany.data
);
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>
);
{/* Total row mobile */}
{selectedYearData.data.length > 0 && (
<Paper p="sm" radius="md" withBorder bg="gray.1">
<Text fz="xs" fw={600} lh={1.4}>Total</Text>
<Flex justify="space-between" mt="xs">
<Text fz="sm" fw={600} lh={1.4}>Kelahiran</Text>
<Text fz="sm" fw={600} lh={1.4}>
{formatNumber(selectedYearData.totalKelahiran)}
</Text>
</Flex>
<Flex justify="space-between" mt="xs">
<Text fz="sm" fw={600} lh={1.4}>Kematian</Text>
<Text fz="sm" fw={600} lh={1.4}>
{formatNumber(selectedYearData.totalKematian)}
</Text>
</Flex>
</Paper>
)}
</Stack>
</Box>
</Stack>
)}
</>
)}
</Stack>
</Paper>
);
}
export default PersentaseDataKelahiranKematian;

View File

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

View File

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

View File

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

View File

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

View File

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

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