Fix QC Kak Inno 22 Des

Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Mobile Device Menu Ekonomi
Fix Search -> useDebounced Menu Ekonomi
This commit is contained in:
2025-12-23 17:18:36 +08:00
parent 29065cb3e2
commit f0f201c853
75 changed files with 3023 additions and 1177 deletions

View File

@@ -0,0 +1,36 @@
// components/modal/ModalKonfirmasiHapus.tsx
import colors from "@/con/colors"
import { Modal, Text, Button, Flex } from "@mantine/core"
interface ModalKonfirmasiNonAktifProps {
opened: boolean
loading?: boolean
onClose: () => void
onConfirm: () => void
text: string
}
export function ModalKonfirmasiNonAktif({
opened,
loading = false,
onClose,
onConfirm,
text,
}: ModalKonfirmasiNonAktifProps) {
return (
<Modal
opened={opened}
onClose={onClose}
title={<Text fw={"bold"} fz={"xl"}>Konfirmasi Non Aktif</Text>}
centered
>
<Text mb="md">{text}</Text>
<Flex justify="flex-end" gap="sm">
<Button style={{color: "white"}} bg={colors['blue-button']} variant="default" onClick={onClose}>Batal</Button>
<Button color="red" onClick={onConfirm} loading={loading}>
Yakin Non Aktif
</Button>
</Flex>
</Modal>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,13 +15,14 @@ import {
TableTh, TableTh,
TableThead, TableThead,
TableTr, TableTr,
Text Text,
Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
@@ -44,6 +45,7 @@ function APBDesa() {
function ListAPBDesa({ search }: { search: string }) { function ListAPBDesa({ search }: { search: string }) {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -61,26 +63,26 @@ function ListAPBDesa({ search }: { search: string }) {
}).format(value); }).format(value);
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors["white-1"]} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors["white-1"]} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Text fw={600} fz="lg"> <Title order={4} lh={1.2}>
List APB Desa List APB Desa
</Text> </Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -94,45 +96,72 @@ function ListAPBDesa({ search }: { search: string }) {
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover> {/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: "15%" }}>Tahun</TableTh> <TableTh style={{ width: "15%" }}>
<TableTh style={{ width: "25%" }}>Pembiayaan</TableTh> <Text fz="sm" fw={600} lh={1.4} ta="left">Tahun</Text>
<TableTh style={{ width: "25%" }}>Belanja</TableTh> </TableTh>
<TableTh style={{ width: "25%" }}>Pendapatan</TableTh> <TableTh style={{ width: "25%" }}>
<TableTh style={{ width: "10%" }}>Aksi</TableTh> <Text fz="sm" fw={600} lh={1.4} ta="left">Pembiayaan</Text>
</TableTh>
<TableTh style={{ width: "25%" }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">Belanja</Text>
</TableTh>
<TableTh style={{ width: "25%" }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">Pendapatan</Text>
</TableTh>
<TableTh style={{ width: "10%" }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">Aksi</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd> <TableTd>
{formatRupiah( <Text fz="md" fw={500} lh={1.5} ta="left">{item.tahun}</Text>
item.pembiayaan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd> </TableTd>
<TableTd> <TableTd>
{formatRupiah( <Text fz="md" fw={500} lh={1.5} ta="left">
item.belanja.reduce( {formatRupiah(
(sum, val) => sum + Number(val.value), item.pembiayaan.reduce(
0 (sum, val) => sum + Number(val.value),
) 0
)} )
)}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
{formatRupiah( <Text fz="md" fw={500} lh={1.5} ta="left">
item.pendapatan.reduce( {formatRupiah(
(sum, val) => sum + Number(val.value), item.belanja.reduce(
0 (sum, val) => sum + Number(val.value),
) 0
)} )
)}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5} ta="left">
{formatRupiah(
item.pendapatan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -143,9 +172,12 @@ function ListAPBDesa({ search }: { search: string }) {
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}` `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
) )
} }
size="compact-sm"
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={16} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -154,7 +186,7 @@ function ListAPBDesa({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py={20}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data APB Desa yang cocok Tidak ada data APB Desa yang cocok
</Text> </Text>
</Center> </Center>
@@ -164,7 +196,81 @@ function ListAPBDesa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Tahun</Text>
<Text fz="sm" fw={500} lh={1.4}>{item.tahun}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Pembiayaan</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(
item.pembiayaan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Belanja</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(
item.belanja.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Pendapatan</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(
item.pendapatan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</Box>
<Button
variant="light"
color="green"
fullWidth
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
)
}
size="sm"
>
<IconDeviceImacCog size={16} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data APB Desa yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -31,7 +31,7 @@ import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function Belanja() { function Belanja() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Stack gap="xl">
<HeaderSearch <HeaderSearch
title="Belanja" title="Belanja"
placeholder="Cari belanja berdasarkan nama atau nilai..." placeholder="Cari belanja berdasarkan nama atau nilai..."
@@ -40,7 +40,7 @@ function Belanja() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListBelanja search={search} /> <ListBelanja search={search} />
</Box> </Stack>
); );
} }
@@ -49,6 +49,7 @@ function ListBelanja({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -72,29 +73,35 @@ function ListBelanja({ search }: { search: string }) {
belanjaState.delete.byId(selectedId); belanjaState.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack gap="xl">
{/* Desktop Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Belanja</Title> <Title
order={4}
lh={{ base: 1.2, md: 1.1 }}
>
Daftar Belanja
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -107,14 +114,24 @@ function ListBelanja({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box visibleFrom="md">
<Table highlightOnHover striped withTableBorder withRowBorders> <Table
highlightOnHover
miw={0}
striped
withTableBorder
withRowBorders
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh>Nilai</TableTh> <TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh>Persentase</TableTh> <TableTh style={{ width: '20%' }}>Persentase</TableTh>
<TableTh>Aksi</TableTh> <TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -123,15 +140,19 @@ function ListBelanja({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd> <TableTd>
{totalBelanja > 0 <Text fz="md" lh={1.45}>{formatRupiah(item.value)}</Text>
? ((item.value / totalBelanja) * 100).toFixed(0) + '%' </TableTd>
: '0%'} <TableTd>
<Text fz="md" lh={1.45}>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Group gap="xs"> <Group gap="xs">
@@ -165,18 +186,20 @@ function ListBelanja({ search }: { search: string }) {
))} ))}
<TableTr> <TableTr>
<TableTd colSpan={2}> <TableTd colSpan={2}>
<Text fw="bold">Total</Text> <Text fz="md" fw={600} lh={1.45}>Total</Text>
</TableTd> </TableTd>
<TableTd colSpan={2}> <TableTd colSpan={2}>
<Text fw="bold">{formatRupiah(totalBelanja)}</Text> <Text fz="md" fw={600} lh={1.45}>{formatRupiah(totalBelanja)}</Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
</> </>
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text c="dimmed">Tidak ada data belanja yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data belanja yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -186,21 +209,107 @@ function ListBelanja({ search }: { search: string }) {
</Box> </Box>
</Paper> </Paper>
{/* Mobile Cards */}
<Stack visibleFrom="xs" hiddenFrom="md" gap="sm">
{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.4} truncate="end">
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nilai
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(item.value)}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Persentase
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
size="xs"
variant="light"
color="red"
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Paper withBorder p="xl" radius="md">
<Center>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data belanja yang cocok
</Text>
</Center>
</Paper>
)}
{filteredData.length > 0 && (
<Paper withBorder p="md" radius="md">
<Group justify="space-between">
<Text fz="sm" fw={600} lh={1.4}>
Total
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(totalBelanja)}
</Text>
</Group>
</Paper>
)}
</Stack>
{/* Pagination */} {/* Pagination */}
<Center> {(totalPages > 1 || page > 1) && (
<Pagination <Center>
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10, search); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" mt="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
)}
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
@@ -209,7 +318,7 @@ function ListBelanja({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus belanja ini?" text="Apakah anda yakin ingin menghapus belanja ini?"
/> />
</Box> </Stack>
); );
} }

View File

@@ -1,7 +1,30 @@
'use client'
import React from 'react'; import React from 'react';
import LayoutTabs from './_lib/layoutTabs'; import LayoutTabs from './_lib/layoutTabs';
import { usePathname } from 'next/navigation';
import { Box } from '@mantine/core';
function Layout({ children }: { children: React.ReactNode }) { function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return ( return (
<LayoutTabs> <LayoutTabs>
{children} {children}

View File

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

View File

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

View File

@@ -16,9 +16,9 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -48,6 +48,7 @@ function ListPembiayaan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -71,17 +72,17 @@ function ListPembiayaan({ search }: { search: string }) {
pembiayaanState.delete.byId(selectedId); pembiayaanState.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
@@ -90,10 +91,10 @@ function ListPembiayaan({ search }: { search: string }) {
const filteredData = data || []; const filteredData = data || [];
return ( return (
<Box py={10}> <Stack gap="xl" py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pembiayaan</Title> <Title order={4} lh={1.2}>Daftar Pembiayaan</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -106,13 +107,24 @@ function ListPembiayaan({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withTableBorder withRowBorders> <Box visibleFrom="md">
<Table
highlightOnHover
striped
withTableBorder
withRowBorders
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh>Nilai</TableTh> <TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh>Persentase</TableTh> <TableTh style={{ width: '20%' }}>Persentase</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh> <TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -122,15 +134,19 @@ function ListPembiayaan({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd> <TableTd>
{totalPembiayaan > 0 <Text fz="md" fw={500} lh={1.5}>{formatRupiah(item.value)}</Text>
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%' </TableTd>
: '0%'} <TableTd>
<Text fz="md" fw={500} lh={1.5}>
{totalPembiayaan > 0
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Group gap="xs"> <Group gap="xs">
@@ -163,16 +179,20 @@ function ListPembiayaan({ search }: { search: string }) {
{/* Total Row */} {/* Total Row */}
<TableTr> <TableTr>
<TableTd colSpan={2}> <TableTd colSpan={2}>
<Text fw="bold">Total</Text> <Text fz="md" fw={600} lh={1.5}>Total</Text>
</TableTd>
<TableTd colSpan={2}>
<Text fz="md" fw={600} lh={1.5}>{formatRupiah(totalPembiayaan)}</Text>
</TableTd> </TableTd>
<TableTd colSpan={2}>{formatRupiah(totalPembiayaan)}</TableTd>
</TableTr> </TableTr>
</> </>
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text c="dimmed">Tidak ada data pembiayaan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pembiayaan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -180,6 +200,79 @@ function ListPembiayaan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.4} truncate="end">
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(item.value)}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Persentase</Text>
<Text fz="sm" fw={500} lh={1.4}>
{totalPembiayaan > 0
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
color="green"
variant="light"
size="xs"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
color="red"
variant="light"
size="xs"
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pembiayaan yang cocok
</Text>
</Center>
)}
{filteredData.length > 0 && (
<Paper withBorder p="md" radius="md">
<Stack gap={4}>
<Text fz="sm" fw={600} lh={1.4}>Total</Text>
<Text fz="sm" fw={500} lh={1.4}>{formatRupiah(totalPembiayaan)}</Text>
</Stack>
</Paper>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -205,7 +298,7 @@ function ListPembiayaan({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus pembiayaan ini?" text="Apakah anda yakin ingin menghapus pembiayaan ini?"
/> />
</Box> </Stack>
); );
} }

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -49,6 +49,7 @@ function ListPendapatan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -70,19 +71,19 @@ function ListPendapatan({ search }: { search: string }) {
pendapatanState.delete.byId(selectedId); pendapatanState.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
@@ -91,10 +92,12 @@ function ListPendapatan({ search }: { search: string }) {
const totalValue = filteredData.reduce((total, item) => total + item.value, 0); const totalValue = filteredData.reduce((total, item) => total + item.value, 0);
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pendapatan</Title> <Title order={2} lh={1.2}>
Daftar Pendapatan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -107,14 +110,30 @@ function ListPendapatan({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withTableBorder withRowBorders> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '40%' }}>Nama</TableTh> <TableTh style={{ width: '40%' }}>
<TableTh style={{ width: '25%' }}>Nilai</TableTh> <Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<TableTh style={{ width: '15%' }}>Edit</TableTh> </TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh> <TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -123,11 +142,13 @@ function ListPendapatan({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end">
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd> <TableTd>
<Text fz="md" fw={500} lh={1.5}>{formatRupiah(item.value)}</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -135,8 +156,10 @@ function ListPendapatan({ search }: { search: string }) {
onClick={() => onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`) router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
} }
fz="sm"
px="xs"
> >
<IconEdit size={18} /> <IconEdit size={16} />
<Text ml={5}>Edit</Text> <Text ml={5}>Edit</Text>
</Button> </Button>
</TableTd> </TableTd>
@@ -149,8 +172,10 @@ function ListPendapatan({ search }: { search: string }) {
setSelectedId(item.id); setSelectedId(item.id);
setModalHapus(true); setModalHapus(true);
}} }}
fz="sm"
px="xs"
> >
<IconTrash size={18} /> <IconTrash size={16} />
<Text ml={5}>Hapus</Text> <Text ml={5}>Hapus</Text>
</Button> </Button>
</TableTd> </TableTd>
@@ -159,19 +184,21 @@ function ListPendapatan({ search }: { search: string }) {
{/* Row total */} {/* Row total */}
<TableTr> <TableTr>
<TableTd colSpan={1}> <TableTd>
<Text fw={'bold'}>Total</Text> <Text fz="md" fw={700} lh={1.5}>Total</Text>
</TableTd> </TableTd>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Text fw={'bold'}>{formatRupiah(totalValue)}</Text> <Text fz="md" fw={700} lh={1.5}>{formatRupiah(totalValue)}</Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
</> </>
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={24}>
<Text color="dimmed">Tidak ada data pendapatan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pendapatan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -179,23 +206,85 @@ function ListPendapatan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack gap="xs" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder radius="md" p="md">
<Stack gap={4}>
<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}>Nilai</Text>
<Text fz="sm" fw={500} lh={1.4}>{formatRupiah(item.value)}</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
}
>
<IconEdit size={14} />
<Text ml={4}>Edit</Text>
</Button>
<Button
variant="light"
color="red"
size="xs"
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={14} />
<Text ml={4}>Hapus</Text>
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pendapatan yang cocok
</Text>
</Center>
)}
{filteredData.length > 0 && (
<Paper withBorder radius="md" p="md">
<Box>
<Text fz="xs" fw={600} lh={1.4}>Total</Text>
<Text fz="sm" fw={700} lh={1.4}>{formatRupiah(totalValue)}</Text>
</Box>
</Paper>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
<Center> {totalPages > 1 && (
<Pagination <Center mt={{ base: 'sm', md: 'md' }} mb={{ base: 'sm', md: 'md' }}>
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10, search); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" color="blue"
color="blue" radius="md"
radius="md" size="sm"
/> />
</Center> </Center>
)}
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus

View File

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

View File

@@ -0,0 +1,33 @@
'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layoutTabs"
import { Box } from "@mantine/core";
export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return (
<LayoutTabs>
{children}
</LayoutTabs>
)
}

View File

@@ -143,7 +143,7 @@ export default function EditPegawaiBumDes() {
if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id; if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id;
const success = await stateOrganisasi.edit.submit(); const success = await stateOrganisasi.edit.submit();
if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai'); if (success) router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai');
} catch (error) { } catch (error) {
console.error('Error updating pegawai:', error); console.error('Error updating pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
@@ -153,12 +153,12 @@ export default function EditPegawaiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai PPID</Title> <Title order={4} ml="sm" c="dark">Edit Data Pegawai BumDes</Title>
</Group> </Group>
<Paper <Paper

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import { ModalKonfirmasiNonAktif } from '@/app/admin/(dashboard)/_com/modalNonaktif';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
@@ -28,7 +29,7 @@ function DetailPegawai() {
statePegawai.delete.byId(selectedId); statePegawai.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai");
} }
}; };
@@ -37,7 +38,7 @@ function DetailPegawai() {
statePegawai.nonActive.byId(selectedId); statePegawai.nonActive.byId(selectedId);
setModalNonActive(false); setModalNonActive(false);
setSelectedId(null); setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai");
} }
}; };
@@ -52,7 +53,7 @@ function DetailPegawai() {
const data = statePegawai.findUnique.data; const data = statePegawai.findUnique.data;
return ( return (
<Box> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
@@ -60,7 +61,7 @@ function DetailPegawai() {
</Box> </Box>
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -68,7 +69,7 @@ function DetailPegawai() {
> >
<Stack gap="md"> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pegawai PPID Detail Pegawai BumDes
</Text> </Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
@@ -165,7 +166,7 @@ function DetailPegawai() {
<Button <Button
color="green" color="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${data.id}/edit`)} onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/${data.id}/edit`)}
variant="light" variant="light"
radius="md" radius="md"
size="md" size="md"
@@ -187,7 +188,7 @@ function DetailPegawai() {
/> />
{/* Modal NonActive */} {/* Modal NonActive */}
<ModalKonfirmasiHapus <ModalKonfirmasiNonAktif
opened={modalNonActive} opened={modalNonActive}
onClose={() => setModalNonActive(false)} onClose={() => setModalNonActive(false)}
onConfirm={handleNonActive} onConfirm={handleNonActive}

View File

@@ -72,7 +72,7 @@ function CreatePegawaiBumDes() {
// Reset form dan redirect // Reset form dan redirect
resetForm(); resetForm();
toast.success("Data pegawai berhasil ditambahkan"); toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai");
} catch (error) { } catch (error) {
console.error("Error creating pegawai:", error); console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai"); toast.error("Terjadi kesalahan saat menambahkan pegawai");
@@ -82,13 +82,13 @@ function CreatePegawaiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Pegawai BUMDesa Tambah Pegawai BUMDes
</Title> </Title>
</Group> </Group>

View File

@@ -0,0 +1,232 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useDebouncedValue } from '@mantine/hooks';
function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai BUMDes'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawaiBumdes search={search} />
</Box>
);
}
function ListPegawaiBumdes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
// Handle loading state
if (loading || !data) {
return (
<Stack py="md">
<Skeleton height={300} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} visibleFrom="md">Daftar Pegawai BUMDes</Title>
<Title order={3} hiddenFrom="md">Daftar Pegawai BUMDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} ta="center">
Tidak ada data pegawai yang ditemukan
</Text>
</Center>
</Paper>
</Box>
);
}
const sortedData = [...filteredData].sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap);
}
return Number(b.isActive) - Number(a.isActive);
});
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} visibleFrom="md">Daftar Pegawai BUMDes</Title>
<Title order={3} hiddenFrom="md">Daftar Pegawai BUMDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
{/* Desktop: Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '30%' }}>Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Status</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{sortedData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} fz="md" lh={1.45} truncate="end">
{item.namaLengkap}
</Text>
</TableTd>
<TableTd>
<Badge variant="light" color="blue" fz="sm" lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</TableTd>
<TableTd>
<Badge color={item.isActive ? "green" : "red"} fz="sm" lh={1.4}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile: Card List */}
<Stack gap="sm" hiddenFrom="md">
{sortedData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap="xs">
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Lengkap</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.namaLengkap}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Posisi</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
<Group gap="xs">
{item.isActive ? (
<Group gap="xs">
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={14} />
</ThemeIcon>
<Text fz="sm" fw={500} c="green">Aktif</Text>
</Group>
) : (
<Group gap="xs">
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={14} />
</ThemeIcon>
<Text fz="sm" fw={500} c="red">Tidak Aktif</Text>
</Group>
)}
</Group>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/${item.id}`)}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
);
}
export default PegawaiBumDes;

View File

@@ -95,7 +95,7 @@ function EditPosisiOrganisasiBumDes() {
const success = await stateOrganisasi.edit.update(); const success = await stateOrganisasi.edit.update();
if (success) { if (success) {
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi');
} }
} catch (err) { } catch (err) {
console.error('Error updating posisi organisasi:', err); console.error('Error updating posisi organisasi:', err);
@@ -106,7 +106,7 @@ function EditPosisiOrganisasiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -36,7 +36,7 @@ function CreatePosisiOrganisasiBumDes() {
await stateOrganisasi.create.submit(); await stateOrganisasi.create.submit();
toast.success('Posisi organisasi berhasil ditambahkan'); toast.success('Posisi organisasi berhasil ditambahkan');
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi');
} catch (error) { } catch (error) {
toast.error('Gagal menambahkan posisi organisasi'); toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error); console.error('Error:', error);
@@ -46,7 +46,7 @@ function CreatePosisiOrganisasiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -0,0 +1,305 @@
'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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi BUMDes'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPosisiOrganisasiBumDes search={search} />
</Box>
);
}
function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<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={{ base: 1.2, md: 1.1 }}
>
Daftar Posisi Organisasi BumDes
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create'
)
}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Nama Posisi
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Deskripsi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Hierarki
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Edit
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Hapus
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" fw={500} lh={1.45} c="dimmed" lineClamp={2}>
{item.deskripsi || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.45}>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd ta="center">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(
`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd ta="center">
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={{ base: 'sm', md: 'md' }}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<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="sm" fw={600} lh={1.4}>
Nama Posisi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.deskripsi || '-'}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Hierarki
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.hierarki || '-'}
</Text>
</Box>
<Group justify="flex-end" gap="xs" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(
`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="sm">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
{totalPages > 1 && (
<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>
)}
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
</Box>
);
}
export default PosisiOrganisasiBumDes;

View File

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

View File

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

View File

@@ -106,38 +106,52 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Demografi Pekerjaan</Title> <Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
List Demografi Pekerjaan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')} onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
fz={{ base: 'sm', md: 'md' }}
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ minWidth: 200 }}>Pekerjaan</TableTh> <TableTh style={{ width: '40%' }}>Pekerjaan</TableTh>
<TableTh style={{ minWidth: 200 }}>Laki - Laki</TableTh> <TableTh style={{ width: '20%' }}>Laki - Laki</TableTh>
<TableTh style={{ minWidth: 200 }}>Perempuan</TableTh> <TableTh style={{ width: '20%' }}>Perempuan</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ minWidth: 200 }}>{item.pekerjaan}</TableTd> <TableTd>{item.pekerjaan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.lakiLaki}</TableTd> <TableTd>{item.lakiLaki}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.perempuan}</TableTd> <TableTd>{item.perempuan}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -145,8 +159,11 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
onClick={() => onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`) router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
} }
fz="sm"
px="xs"
py="xs"
> >
<IconEdit size={18} /> <IconEdit size={16} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
@@ -158,17 +175,22 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
setSelectedId(item.id); setSelectedId(item.id);
setModalHapus(true); setModalHapus(true);
}} }}
fz="sm"
px="xs"
py="xs"
> >
<IconTrash size={18} /> <IconTrash size={16} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={5}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Tidak ada data demografi pekerjaan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data demografi pekerjaan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -176,6 +198,78 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Pekerjaan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.pekerjaan}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Laki - Laki
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.lakiLaki}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Perempuan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.perempuan}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
fz="xs"
px="xs"
py="xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="xs"
px="xs"
py="xs"
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data demografi pekerjaan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -195,10 +289,13 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</Center> </Center>
{/* Chart */} {/* Chart */}
<Box mt={30} style={{ width: '100%', minHeight: 400 }}> <Box mt={{ base: 'lg', md: 'xl' }}>
<Paper bg={colors['white-1']} p="md" radius="md" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="md" withBorder>
<Stack gap={"xs"}> <Stack gap="xs">
<Title pb={10} order={4}> <Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Grafik Demografi Pekerjaan Grafik Demografi Pekerjaan
</Title> </Title>
{mounted && chartData.length > 0 ? ( {mounted && chartData.length > 0 ? (
@@ -213,17 +310,23 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
]} ]}
/> />
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Group justify='center'> <Group justify="center" gap='md'>
<Flex align="center" gap={10}> <Flex align="center" gap={8}>
<Box bg="#5082EE" w={20} h={20} /> <Box bg="#5082EE" w={16} h={16} />
<Text>Laki - Laki</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Laki - Laki
</Text>
</Flex> </Flex>
<Flex align="center" gap={10}> <Flex align="center" gap={8}>
<Box bg="#6EDF9C" w={20} h={20} /> <Box bg="#6EDF9C" w={16} h={16} />
<Text>Perempuan</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Perempuan
</Text>
</Flex> </Flex>
</Group> </Group>
</Box> </Box>

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
@@ -16,9 +16,9 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -26,12 +26,10 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
// ✅ BarChart Mantine
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
function JumlahPendudukMiskin() { function JumlahPendudukMiskin() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -54,16 +52,15 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, loading, load, totalPages } = stateJPM.findMany; const { data, page, loading, load, totalPages } = stateJPM.findMany;
// Load data awal
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
// Update chart data
useEffect(() => { useEffect(() => {
if (stateJPM.findMany.data) { if (stateJPM.findMany.data) {
setChartData( setChartData(
@@ -88,18 +85,20 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack py={{ base: 'sm', md: 'md' }} gap='lg'>
{/* Tabel */} {/* Main Table/Card Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Jumlah Penduduk Miskin</Title> <Title order={4} lh={1.2}>
Daftar Jumlah Penduduk Miskin
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -112,22 +111,54 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Tahun</TableTh> <TableTh style={{ width: '25%' }}>
<TableTh style={{ width: '35%' }}>Jumlah Penduduk Miskin</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Edit</TableTh> Tahun
<TableTh style={{ width: '20%' }}>Delete</TableTh> </Text>
</TableTh>
<TableTh style={{ width: '35%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Penduduk Miskin
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Edit
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Delete
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.year}</TableTd> <TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd> <Text fz="md" fw={500} lh={1.5}>
{item.year}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.totalPoorPopulation}
</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -158,7 +189,9 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Tidak ada data yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -166,6 +199,64 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack hiddenFrom="md" gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.year}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Penduduk Miskin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.totalPoorPopulation}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)
}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -185,9 +276,9 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Center> </Center>
{/* Bar Chart */} {/* Bar Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md"> <Paper bg={colors['white-1']} p={{ base: 'sm', md: 'md' }} mt="lg" withBorder radius="md">
<Stack> <Stack gap="xs">
<Title order={4} mb="sm"> <Title order={4} lh={1.2} mb="sm">
Grafik Jumlah Penduduk Miskin Grafik Jumlah Penduduk Miskin
</Title> </Title>
{mounted && chartData.length > 0 ? ( {mounted && chartData.length > 0 ? (
@@ -198,14 +289,14 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
value: item.totalPoorPopulation, value: item.totalPoorPopulation,
}))} }))}
dataKey="name" dataKey="name"
series={[ series={[{ name: 'value', color: colors['blue-button'] }]}
{ name: 'value', color: colors['blue-button'] },
]}
withTooltip withTooltip
valueFormatter={(v) => `${v.toLocaleString()} jiwa`} valueFormatter={(v) => `${v.toLocaleString()} jiwa`}
/> />
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Stack> </Stack>
</Paper> </Paper>
@@ -217,7 +308,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus data ini?" text="Apakah anda yakin ingin menghapus data ini?"
/> />
</Box> </Stack>
); );
} }

View File

@@ -2,6 +2,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box,
ScrollArea, ScrollArea,
Stack, Stack,
Tabs, Tabs,
@@ -61,36 +62,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
style={{ p="sm"
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", style={{
borderRadius: "1rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", borderRadius: "1rem",
display: "flex", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
flexWrap: "nowrap", display: "flex",
gap: "0.5rem", flexWrap: "nowrap",
paddingInline: "0.5rem", gap: "0.5rem",
}} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
> >
{tabs.map((tab, i) => (
<TabsTab <TabsList
key={i} p="xs" // lebih kecil
value={tab.value} style={{
leftSection={tab.icon} background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
style={{ borderRadius: "1rem",
fontWeight: 600, display: "flex",
fontSize: "0.9rem", flexWrap: "nowrap",
transition: "all 0.2s ease", gap: "0.5rem",
flexShrink: 0, width: "max-content", // ⬅️ kunci
}} maxWidth: "100%", // ⬅️ penting
> }}
{tab.label} >
</TabsTab> {tabs.map((tab, i) => (
))} <TabsTab
</TabsList> key={i}
</ScrollArea> value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,6 +1,28 @@
'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layoutTabs"; import LayoutTabs from "./_lib/layoutTabs";
import { Box } from "@mantine/core";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return ( return (
<LayoutTabs> <LayoutTabs>
{children} {children}

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import {
Button, Button,
Center, Center,
Flex, Flex,
Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -20,7 +21,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -51,6 +52,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
@@ -64,8 +66,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = stategrafik.findMany; const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -103,18 +105,20 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'lg' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack py={{ base: 'sm', md: 'lg' }} gap='md'>
{/* Table Data */} {/* Section: List Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Flex justify="space-between" align="center" mb="md"> <Flex visibleFrom='md' justify="space-between" align="center" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Pendidikan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -129,17 +133,43 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</Button> </Button>
</Flex> </Flex>
<Box style={{ overflowX: 'auto' }}> <Group hiddenFrom='md' align="center" mb={{ base: 'sm', md: 'md' }}>
<Table highlightOnHover> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Pendidikan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create',
)
}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>SD</TableTh> <TableTh style={{ width: '16%' }}>SD</TableTh>
<TableTh>SMP</TableTh> <TableTh style={{ width: '16%' }}>SMP</TableTh>
<TableTh>SMA</TableTh> <TableTh style={{ width: '16%' }}>SMA</TableTh>
<TableTh>D3</TableTh> <TableTh style={{ width: '16%' }}>D3</TableTh>
<TableTh>S1</TableTh> <TableTh style={{ width: '16%' }}>S1</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh style={{ width: '10%' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -147,7 +177,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={7}> <TableTd colSpan={7}>
<Center py={20}> <Center py={20}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden Belum ada data grafik responden
</Text> </Text>
</Center> </Center>
@@ -156,11 +186,31 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
) : ( ) : (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.SD}</TableTd> <TableTd>
<TableTd>{item.SMP}</TableTd> <Text fz="md" fw={500} lh={1.5}>
<TableTd>{item.SMA}</TableTd> {item.SD}
<TableTd>{item.D3}</TableTd> </Text>
<TableTd>{item.S1}</TableTd> </TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.SMP}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.SMA}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.D3}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.S1}
</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
color="green" color="green"
@@ -193,6 +243,92 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{filteredData.length === 0 ? (
<Center py="sm">
<Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden
</Text>
</Center>
) : (
<Stack gap="sm">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
SD
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.SD}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
SMP
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.SMP}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
SMA
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.SMA}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
D3
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.D3}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
S1
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.S1}
</Text>
</Box>
<Flex gap="xs" mt="xs">
<Button
size="compact-sm"
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`,
)
}
>
<IconEdit size={16} />
</Button>
<Button
size="compact-sm"
color="red"
variant="light"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Flex>
</Stack>
</Paper>
))}
</Stack>
)}
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -211,10 +347,10 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
/> />
</Center> </Center>
{/* Donut Chart */} {/* Section: Donut Chart */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mt="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Stack> <Stack gap="md">
<Title order={3} pb={10}> <Title order={4} lh={1.2}>
Grafik Pengangguran Berdasarkan Pendidikan Grafik Pengangguran Berdasarkan Pendidikan
</Title> </Title>
<Center> <Center>
@@ -228,7 +364,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
thickness={40} thickness={40}
/> />
) : ( ) : (
<Text color="dimmed"> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
)} )}
@@ -243,7 +379,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?" text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?"
/> />
</Box> </Stack>
); );
} }

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Center, Center,
Flex, Flex,
Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -19,7 +20,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -51,6 +52,7 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
@@ -64,8 +66,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const { data, page, totalPages, loading, load } = stategrafik.findMany; const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -87,19 +89,21 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
{/* Table */} {/* Table - Desktop */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mb="lg">
<Stack> <Stack gap="md">
<Flex justify="space-between" align="center" mb="md"> <Flex justify="space-between" align="center" visibleFrom='md'>
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Usia Kerja
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -112,26 +116,58 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</Button> </Button>
</Flex> </Flex>
<Box style={{ overflowX: 'auto' }}> <Group align="center" hiddenFrom='md'>
<Table highlightOnHover> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Usia Kerja
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')
}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Usia 18-25</TableTh> <TableTh style={{ width: '20%' }}>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh> <TableTh style={{ width: '20%' }}>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh> <TableTh style={{ width: '20%' }}>Usia 36-45</TableTh>
<TableTh>Usia 46+</TableTh> <TableTh style={{ width: '20%' }}>Usia 46+</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh style={{ width: '10%' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd> <TableTd fz="md" fw={500} lh={1.5}>
<TableTd>{item.usia26_35}</TableTd> {item.usia18_25}
<TableTd>{item.usia36_45}</TableTd> </TableTd>
<TableTd>{item.usia46_keatas}</TableTd> <TableTd fz="md" fw={500} lh={1.5}>
{item.usia26_35}
</TableTd>
<TableTd fz="md" fw={500} lh={1.5}>
{item.usia36_45}
</TableTd>
<TableTd fz="md" fw={500} lh={1.5}>
{item.usia46_keatas}
</TableTd>
<TableTd> <TableTd>
<Button <Button
color="green" color="green"
@@ -160,7 +196,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTd colSpan={6}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -168,6 +206,80 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
<Stack gap="xs">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 18-25
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia18_25}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 26-35
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia26_35}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 36-45
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia36_45}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 46+
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia46_keatas}
</Text>
</Box>
<Flex gap="xs" mt="xs">
<Button
size="xs"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)
}
>
<IconEdit size={16} />
</Button>
<Button
size="xs"
color="red"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Flex>
</Stack>
</Paper>
))}
</Stack>
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden
</Text>
</Center>
)}
</Box>
</Stack> </Stack>
</Paper> </Paper>
@@ -189,8 +301,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
{/* Donut Chart */} {/* Donut Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md"> <Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack> <Stack gap="md">
<Title order={3} pb={10}> <Title order={4} lh={1.2}>
Grafik Pengangguran Berdasarkan Usia Kerja Grafik Pengangguran Berdasarkan Usia Kerja
</Title> </Title>
{donutData.length > 0 ? ( {donutData.length > 0 ? (
@@ -205,7 +317,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
/> />
</Center> </Center>
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Stack> </Stack>
</Paper> </Paper>

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import {
Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title Text, Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -20,7 +20,7 @@ function DetailDataPengangguran() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Stack>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran' title='Detail Data Pengangguran'
placeholder='Cari bulan atau tahun...' placeholder='Cari bulan atau tahun...'
@@ -29,7 +29,7 @@ function DetailDataPengangguran() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListDetailDataPengangguran search={search} /> <ListDetailDataPengangguran search={search} />
</Box> </Stack>
); );
} }
@@ -38,6 +38,7 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -49,8 +50,8 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -68,23 +69,25 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
} }
}, [data]); }, [data]);
const filteredData = data || [] const filteredData = data || [];
// Loading state // Loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py="md"> <Stack py={{ base: 'md', md: 'lg' }} gap="lg">
<Skeleton h={500} radius="md" /> <Skeleton h={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Stack py="md" gap="lg"> <Stack py={{ base: 'md', md: 'lg' }} gap="lg">
{/* Table Section */} {/* Table / Card Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Detail Data Pengangguran</Title> <Title order={4} lh={1.2}>
Daftar Detail Data Pengangguran
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -95,23 +98,45 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withTableBorder withRowBorders> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
withTableBorder
withRowBorders
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Bulan</TableTh> <TableTh style={{ width: '30%' }}>Bulan</TableTh>
<TableTh style={{ width: '20%' }}>Terdidik</TableTh> <TableTh style={{ width: '25%' }}>Terdidik</TableTh>
<TableTh style={{ width: '20%' }}>Tidak Terdidik</TableTh> <TableTh style={{ width: '25%' }}>Tidak Terdidik</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.month} {item.year}</TableTd> <TableTd>
<TableTd>{item.educatedUnemployment}</TableTd> <Text fz="md" fw={500} lh={1.5}>
<TableTd>{item.uneducatedUnemployment}</TableTd> {item.month} {item.year}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.educatedUnemployment}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.uneducatedUnemployment}
</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -119,7 +144,9 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)} onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -128,7 +155,9 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text c="dimmed">Tidak ada data yang cocok</Text> <Text c="dimmed" fz="sm" fw={500} lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -136,25 +165,85 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Bulan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.month} {item.year}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Terdidik
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.educatedUnemployment}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tidak Terdidik
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.uneducatedUnemployment}
</Text>
</Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
mt="xs"
>
<IconDeviceImac size={18} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" fw={500} lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center>
<Pagination {/* Pagination */}
value={page} {totalPages > 1 && (
onChange={(newPage) => { <Center>
load(newPage, 10); <Pagination
window.scrollTo({ top: 0, behavior: 'smooth' }); value={page}
}} onChange={(newPage) => {
total={totalPages} load(newPage, 10);
mt="md" window.scrollTo({ top: 0, behavior: 'smooth' });
mb="md" }}
color="blue" total={totalPages}
radius="md" mt="md"
/> mb="md"
</Center> color="blue"
radius="md"
/>
</Center>
)}
{/* Chart Section */} {/* Chart Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Title order={4} mb="md"> <Title order={4} lh={1.2} mb={{ base: 'sm', md: 'md' }}>
Data Pengangguran Terdidik & Tidak Terdidik Data Pengangguran Terdidik & Tidak Terdidik
</Title> </Title>
{mounted && chartData.length > 0 ? ( {mounted && chartData.length > 0 ? (
@@ -170,7 +259,9 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
/> />
</Box> </Box>
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'sm', md: 'md' }} fw={500} lh={1.5}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Paper> </Paper>
</Stack> </Stack>

View File

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

View File

@@ -42,7 +42,7 @@ function DetailLowonganKerjaLokal() {
const data = lowonganState.findUnique.data; const data = lowonganState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -99,12 +99,16 @@ function DetailLowonganKerjaLokal() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} /> <Box pl={8}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
</Box>
</Box> </Box>
<Box> <Box>
<Text fz="lg" fw="bold">Kualifikasi</Text> <Text fz="lg" fw="bold">Kualifikasi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }} /> <Box pl={8}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }} />
</Box>
</Box> </Box>
<Group gap="sm" mt="sm"> <Group gap="sm" mt="sm">

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -46,70 +46,87 @@ function LowonganKerjaLokal() {
function ListLowonganKerjaLokal({ search }: { search: string }) { function ListLowonganKerjaLokal({ search }: { search: string }) {
const stateLowongan = useProxy(lowonganKerjaState); const stateLowongan = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = stateLowongan.findMany; const { data, page, totalPages, loading, load } = stateLowongan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="lg">
<Title order={4}>Daftar Lowongan Kerja Lokal</Title> <Title order={4}>Daftar Lowongan Kerja Lokal</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => onClick={() =>
router.push('/admin/ekonomi/lowongan-kerja-lokal/create') router.push('/admin/ekonomi/lowongan-kerja-lokal/create')
} }
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Pekerjaan</TableTh> <TableTh style={{ width: '25%' }}>
<TableTh style={{ width: '25%' }}>Nama Perusahaan</TableTh> <Text fz="sm" fw={600} lh={1.2} c="black">Pekerjaan</Text>
<TableTh style={{ width: '20%' }}>Lokasi</TableTh> </TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.2} c="black">Nama Perusahaan</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.2} c="black">Lokasi</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.2} c="black">Aksi</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '25%' }}> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.posisi} {item.posisi}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '25%' }}> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Text fz="sm" fw={500} lh={1.5} truncate="end">
{item.namaPerusahaan} {item.namaPerusahaan}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Text fz="sm" fw={500} lh={1.5} truncate="end">
{item.lokasi} {item.lokasi}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
@@ -118,9 +135,11 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
`/admin/ekonomi/lowongan-kerja-lokal/${item.id}` `/admin/ekonomi/lowongan-kerja-lokal/${item.id}`
) )
} }
fullWidth
radius="sm"
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml="xs">Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -128,8 +147,8 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text fz="sm" c="dimmed" lh={1.4}>
Tidak ada data lowongan kerja yang cocok Tidak ada data lowongan kerja yang cocok
</Text> </Text>
</Center> </Center>
@@ -139,6 +158,57 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card List */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Pekerjaan</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.posisi}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Perusahaan</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.namaPerusahaan}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Lokasi</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.lokasi}
</Text>
</Box>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`
)
}
fullWidth
radius="sm"
mt="xs"
>
<IconDeviceImac size={20} />
<Text ml="xs">Detail</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text fz="sm" c="dimmed" lh={1.4}>
Tidak ada data lowongan kerja yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect } from '@mantine/hooks'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -10,9 +27,8 @@ import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa'; import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function KategoriProduk() { function KategoriProduk() {
const [search2, setSearch2] = useState("") const [search2, setSearch2] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -28,63 +44,82 @@ function KategoriProduk() {
} }
function ListKategoriProduk({ search2 }: { search2: string }) { function ListKategoriProduk({ search2 }: { search2: string }) {
const statePasar = useProxy(pasarDesaState.kategoriProduk) const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter() const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search2, 1000);
const { const { data, page, totalPages, loading, load } = statePasar.findMany;
data,
page,
totalPages,
loading,
load,
} = statePasar.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search2) load(page, 10, debouncedSearch);
}, [page, search2]) }, [page, debouncedSearch]);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
statePasar.delete.byId(selectedId) statePasar.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
} }
} };
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Kategori Produk</Title> <Title order={4} lh={1.2}>
<Button Daftar Kategori Produk
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')} color="blue"
> variant="light"
Tambah Baru onClick={() =>
</Button> router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')
}
>
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '60%' }}>Nama Kategori</TableTh> <TableTh style={{ width: '60%' }}>
<TableTh style={{ width: '20%' }}>Edit</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Delete</TableTh> Nama Kategori
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Edit
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Delete
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -92,38 +127,48 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end">
{item.nama} {item.nama}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Center>
<Button <Button
color="green" color="green"
variant="light" variant="light"
onClick={() => router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)} onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`
)
}
> >
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</Center>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Center>
<Button <Button
color="red" color="red"
variant="light" variant="light"
onClick={() => { onClick={() => {
setSelectedId(item.id) setSelectedId(item.id);
setModalHapus(true) setModalHapus(true);
}} }}
> >
<IconX size={18} /> <IconX size={18} />
</Button> </Button>
</TableTd> </Center>
</TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed">Tidak ada data kategori produk yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori produk yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -131,14 +176,69 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
<Stack gap="sm">
{filteredData.map((item) => (
<Paper
key={item.id}
withBorder
p="md"
radius="md"
bg={colors['white-1']}
>
<Box mb="xs">
<Text fz="sm" fw={600} lh={1.4}>
Nama Kategori
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Group justify="flex-end" mt="md">
<Button
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
<Button
color="red"
variant="light"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={18} />
</Button>
</Group>
</Paper>
))}
</Stack>
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori produk yang cocok
</Text>
</Center>
)}
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10) load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
@@ -156,7 +256,7 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
text='Apakah anda yakin ingin menghapus kategori produk ini?' text='Apakah anda yakin ingin menghapus kategori produk ini?'
/> />
</Box> </Box>
) );
} }
export default KategoriProduk; export default KategoriProduk;

View File

@@ -1,9 +1,29 @@
'use client' 'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layoutTabs" import LayoutTabs from "./_lib/layoutTabs"
import { Box } from "@mantine/core";
export default function Layout({children} : {children: React.ReactNode}) { export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return ( return (
<LayoutTabs> <LayoutTabs>
{children} {children}

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,28 +45,29 @@ function PasarDesa() {
function ListPasarDesa({ search }: { search: string }) { function ListPasarDesa({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.pasarDesa); const statePasar = useProxy(pasarDesaState.pasarDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = statePasar.findMany; const { data, page, totalPages, loading, load } = statePasar.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Produk Pasar Desa</Title> <Title order={4} lh={1.2}>Daftar Produk Pasar Desa</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -79,15 +80,23 @@ function ListPasarDesa({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Nama Produk</TableTh> <TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Nama Produk</Text></TableTh>
<TableTh style={{ width: '20%' }}>Harga Produk</TableTh> <TableTh style={{ width: '20%' }}><Text fz="sm" fw={600} lh={1.4}>Harga Produk</Text></TableTh>
<TableTh style={{ width: '15%' }}>Rating</TableTh> <TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Rating</Text></TableTh>
<TableTh style={{ width: '25%' }}>Alamat Usaha</TableTh> <TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Alamat Usaha</Text></TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -95,18 +104,18 @@ function ListPasarDesa({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.nama} {item.nama}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text>Rp.{item.harga}</Text> <Text fz="md" lh={1.5}>Rp.{item.harga}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text>{item.rating || '-'}</Text> <Text fz="md" lh={1.5}>{item.rating || '-'}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Text fz="sm" lh={1.5} c="dimmed">
{item.alamatUsaha || '-'} {item.alamatUsaha || '-'}
</Text> </Text>
</TableTd> </TableTd>
@@ -121,7 +130,7 @@ function ListPasarDesa({ search }: { search: string }) {
} }
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -129,8 +138,8 @@ function ListPasarDesa({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py={32}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada produk pasar desa yang cocok Tidak ada produk pasar desa yang cocok
</Text> </Text>
</Center> </Center>
@@ -140,6 +149,57 @@ function ListPasarDesa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="sm">
{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 Produk</Text>
<Text fz="sm" fw={500} lh={1.4}>{item.nama}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Harga Produk</Text>
<Text fz="sm" fw={500} lh={1.4}>Rp.{item.harga}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Rating</Text>
<Text fz="sm" fw={500} lh={1.4}>{item.rating || '-'}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Alamat Usaha</Text>
<Text fz="sm" fw={500} lh={1.4} c="dimmed">
{item.alamatUsaha || '-'}
</Text>
</Box>
<Box>
<Button
fullWidth
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={32}>
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada produk pasar desa yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,43 @@
'use client' 'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect } from '@mantine/hooks'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, XAxis, YAxis } from 'recharts'; import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip as RechartTooltip,
XAxis,
YAxis,
} from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan'; import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
function ProgramKemiskinan() { function ProgramKemiskinan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -32,21 +57,22 @@ function ListProgramKemiskinan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = programState.findMany; const { data, page, totalPages, loading, load } = programState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const chartData = data const chartData = data
.filter(item => item.statistik) .filter((item) => item.statistik)
.map(item => ({ .map((item) => ({
tahun: item.statistik?.tahun, tahun: item.statistik?.tahun,
jumlah: Number(item.statistik?.jumlah) jumlah: Number(item.statistik?.jumlah),
})) }))
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); .sort((a, b) => (a.tahun || 0) - (b.tahun || 0));
@@ -58,49 +84,90 @@ function ListProgramKemiskinan({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> {/* Daftar Program Kemiskinan */}
<Group justify="space-between" mb="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title order={4}>Daftar Program Kemiskinan</Title> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}> <Title order={4} lh={1.2}>
Daftar Program Kemiskinan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}
>
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover> {/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Judul Program</TableTh> <TableTh style={{ width: '30%' }}>
<TableTh style={{ width: '40%' }}>Deskripsi Singkat</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Jumlah Masyarakat Miskin</TableTh> Judul Program
<TableTh style={{ width: '10%' }}>Aksi</TableTh> </Text>
</TableTh>
<TableTh style={{ width: '40%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Masyarakat Miskin
</Text>
</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map(item => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate lineClamp={1}>{item.nama}</Text> <Text fw={500} fz="md" lh={1.45} truncate lineClamp={1}>
{item.nama}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" truncate lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text fz="sm" lh={1.45} truncate lineClamp={2} c="dark.9" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Text fz="md" lh={1.45} fw={500}>
{item.statistik?.jumlah || '-'}
</Text>
</TableTd> </TableTd>
<TableTd>{item.statistik?.jumlah || '-'}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)} onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
fz="sm"
lh={1.4}
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={18} />
<Text ml={5}>Detail</Text> <Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
@@ -110,7 +177,9 @@ function ListProgramKemiskinan({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Tidak ada data program kemiskinan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data program kemiskinan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -118,6 +187,61 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap={4}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Judul Program
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Text fz="sm" fw={500} lh={1.4} c="dark.9" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Masyarakat Miskin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.statistik?.jumlah || '-'}
</Text>
</Box>
<Box>
<Button
fullWidth
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
fz="sm"
lh={1.4}
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data program kemiskinan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -137,25 +261,45 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</Center> </Center>
{/* Chart */} {/* Chart */}
<Box py={10}> <Box pt={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title> <Title order={4} lh={1.2} pb={{ base: 'sm', md: 'md' }}>
Grafik Berdasarkan Responden
</Title>
{mounted && lineChart.length > 0 ? ( {mounted && lineChart.length > 0 ? (
<Box style={{ width: '100%', overflowX: 'auto' }}> <Box>
<LineChart width={820} height={300} data={lineChart}> <Box
<CartesianGrid strokeDasharray="3 3" /> component="div"
<XAxis dataKey="tahun" /> miw={{ base: 320, md: 820 }}
<YAxis /> mx="auto"
<RechartTooltip style={{ overflowX: 'auto' }}
formatter={(value: any, name: string) => [`${value} orang`, name]} >
labelFormatter={(label: any) => `Tahun: ${label}`} <LineChart
/> width={Math.max(320, lineChart.length * 60)}
<Legend /> height={300}
<Line type="monotone" dataKey="jumlah" name="Jumlah per Tahun" stroke={colors['blue-button']} /> data={lineChart}
</LineChart> >
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<RechartTooltip
formatter={(value: any) => [`${value} orang`, 'Jumlah']}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah per Tahun"
stroke={colors['blue-button']}
/>
</LineChart>
</Box>
</Box> </Box>
) : ( ) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Paper> </Paper>
</Box> </Box>

View File

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

View File

@@ -48,7 +48,7 @@ function DetailSektorUnggulanDesa() {
const data = stateGrafik.findUnique.data; const data = stateGrafik.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol kembali */} {/* Tombol kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -61,7 +61,7 @@ function DetailSektorUnggulanDesa() {
<Paper <Paper
withBorder withBorder
w={{ base: '100%', md: '60%' }} w={{ base: '100%', md: '70%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -81,7 +81,9 @@ function DetailSektorUnggulanDesa() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text ta={"justify"} fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.description || '-' }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> <Box pl={8}>
<Text ta={"justify"} fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.description || '-' }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</Box> </Box>
<Box> <Box>

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -58,6 +58,8 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
load, load,
} = state.findMany; } = state.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000);
useEffect(() => { useEffect(() => {
if (state.findMany.data) { if (state.findMany.data) {
setChartData( setChartData(
@@ -72,14 +74,14 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
}, [state.findMany.data]); }, [state.findMany.data]);
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
@@ -87,69 +89,131 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
return ( return (
<Stack gap="md" py="md"> <Stack gap="md" py="md">
{/* List Table */} {/* List Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Sektor Unggulan Desa</Title> <Title order={4} lh={1.2}>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}> List Sektor Unggulan Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}
>
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
{loading ? (
<Skeleton height={300} radius="md" /> {/* Desktop Table */}
) : ( <Box visibleFrom="md">
<Box style={{ overflowX: 'auto' }}> <Table
<Table highlightOnHover> highlightOnHover
<TableThead> miw={0}
<TableTr> style={{
<TableTh style={{ width: '30%' }}>Nama Sektor</TableTh> tableLayout: 'fixed',
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh> width: '100%',
<TableTh style={{ width: '15%' }}>Detail</TableTh> }}
</TableTr> >
</TableThead> <TableThead>
<TableTbody> <TableTr>
{filteredData.length > 0 ? ( <TableTh style={{ width: '35%' }}>Nama Sektor</TableTh>
filteredData.map((item) => ( <TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTr key={item.id}> <TableTh style={{ width: '20%' }}>Detail</TableTh>
<TableTd> </TableTr>
<Box w={200}> </TableThead>
<Text fw={500} truncate="end" lineClamp={1}> <TableTbody>
{item.name} {filteredData.length > 0 ? (
</Text> filteredData.map((item) => (
</Box> <TableTr key={item.id}>
</TableTd> <TableTd>
<TableTd> <Text fz="md" fw={500} lh={1.45} truncate="end">
<Box w={200}> {item.name}
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }} /> </Text>
</Box> </TableTd>
</TableTd> <TableTd>
<TableTd> <Text fz="sm" lineClamp={3} fw={500} lh={1.4} c={item.description ? 'inherit' : 'dimmed'} dangerouslySetInnerHTML={{ __html: item.description || '-' }} />
<Button </TableTd>
variant="light" <TableTd>
color="blue" <Button
onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)} variant="light"
> color="blue"
<IconDeviceImac size={20} /> onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
<Text ml={6}>Detail</Text> radius="md"
</Button> fz="sm"
</TableTd> px="sm"
</TableTr> >
)) <IconDeviceImac size={18} />
) : ( <Text ml={6}>Detail</Text>
<TableTr> </Button>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data sektor unggulan yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
)} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={3}>
)} <Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data sektor unggulan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{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 Sektor
</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" lineClamp={3} fw={500} lh={1.4} c={item.description ? 'inherit' : 'dimmed'} dangerouslySetInnerHTML={{ __html: item.description || '-' }} />
</Box>
</Box>
<Box>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
fullWidth
mt="xs"
radius="md"
fz="sm"
>
<IconDeviceImac size={18} />
<Text ml={6}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data sektor unggulan yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -158,22 +222,20 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md"
mb="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
{/* Chart */} {/* Chart Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title order={4} pb="sm"> <Title order={4} lh={1.2} pb={{ base: 'sm', md: 'md' }}>
Grafik Sektor Unggulan Desa Grafik Sektor Unggulan Desa
</Title> </Title>
{loading ? ( {loading ? (
<Skeleton height={350} radius="md" /> <Skeleton height={350} radius="md" />
) : chartData.length > 0 ? ( ) : chartData.length > 0 ? (
<Box style={{ width: '100%', height: 400 }}> <Box style={{ width: '100%', height: 350 }}>
<ResponsiveContainer> <ResponsiveContainer>
<BarChart data={chartData}> <BarChart data={chartData}>
<XAxis dataKey="name" /> <XAxis dataKey="name" />
@@ -186,7 +248,9 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
</Box> </Box>
) : ( ) : (
<Center py={50}> <Center py={50}>
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
</Center> </Center>
)} )}
</Paper> </Paper>

View File

@@ -1,131 +0,0 @@
/* 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 {
IconBuildingCommunity,
IconHierarchy,
IconUsers
} from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai",
icon: <IconUsers size={18} stroke={1.8} />
},
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi",
icon: <IconBuildingCommunity size={18} stroke={1.8} />
}
];
const currentTab = tabs.find((tab) => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(
currentTab?.value || tabs[0].value
);
const handleTabChange = (value: string | null) => {
const tab = tabs.find((t) => t.value === value);
if (tab) {
router.push(tab.href);
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find((tab) => tab.href === pathname);
if (match) {
setActiveTab(match.value);
}
}, [pathname]);
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Struktur Organisasi & SK Pengurus BUMDesa
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal biar rapi kalau label panjang */}
<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",
}}
>
{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,
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
</Stack >
);
}
export default LayoutTabs;

View File

@@ -1,12 +0,0 @@
'use client'
import LayoutTabs from "./_lib/layoutTabs"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
)
}

View File

@@ -1,185 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai BUMDesa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawaiBumdes search={search} />
</Box>
);
}
function ListPegawaiBumdes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
<TableTh style={{ width: '10%' }}>Status</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
<Badge color={item.isActive ? "green" : "red"}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Box hiddenFrom="sm">
{item.isActive ? (
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={16} />
</ThemeIcon>
) : (
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={16} />
</ThemeIcon>
)}
</Box>
</Group>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
);
}
export default PegawaiBumDes;

View File

@@ -1,169 +0,0 @@
'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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi BUMDes'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPosisiOrganisasiBumDes search={search} />
</Box>
);
}
function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<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 Posisi Organisasi BumDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</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>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
</Box>
);
}
export default PosisiOrganisasiBumDes;

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import { ModalKonfirmasiNonAktif } from '@/app/admin/(dashboard)/_com/modalNonaktif';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
@@ -186,7 +187,7 @@ function DetailPegawai() {
/> />
{/* Modal NonActive */} {/* Modal NonActive */}
<ModalKonfirmasiHapus <ModalKonfirmasiNonAktif
opened={modalNonActive} opened={modalNonActive}
onClose={() => setModalNonActive(false)} onClose={() => setModalNonActive(false)}
onConfirm={handleNonActive} onConfirm={handleNonActive}

View File

@@ -223,8 +223,8 @@ export const devBar = [
}, },
{ {
id: "Ekonomi_3", id: "Ekonomi_3",
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa", name: "Struktur Organisasi Dan Sk Pengurus BumDes",
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai" path: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai"
}, },
{ {
id: "Ekonomi_4", id: "Ekonomi_4",
@@ -628,7 +628,7 @@ export const navBar = [
{ {
id: "Ekonomi_3", id: "Ekonomi_3",
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa", name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai" path: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai"
}, },
{ {
id: "Ekonomi_4", id: "Ekonomi_4",
@@ -990,7 +990,7 @@ export const role1 = [
{ {
id: "Ekonomi_3", id: "Ekonomi_3",
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa", name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai" path: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai"
}, },
{ {
id: "Ekonomi_4", id: "Ekonomi_4",

View File

@@ -509,7 +509,7 @@ function NodeCard({ node, router }: any) {
mt={8} mt={8}
radius="md" radius="md"
onClick={() => onClick={() =>
router.push(`/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/${node.data.id}`) router.push(`/darmasaba/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/${node.data.id}`)
} }
style={{ style={{
height: 32, height: 32,

View File

@@ -47,7 +47,7 @@ const getDetailUrl = (item: { type?: string; id: string | number;[key: string]:
tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan', tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan',
pasarDesa: () => '/darmasaba/ekonomi/pasar-desa', pasarDesa: () => '/darmasaba/ekonomi/pasar-desa',
lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal', lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal',
strukturOrganisasi: () => '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa', strukturOrganisasi: () => '/darmasaba/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes',
jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur', jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur', jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin', jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin',

View File

@@ -183,7 +183,7 @@ const navbarListMenu = [
{ {
id: "5.3", id: "5.3",
name: "Struktur Organisasi dan SK Pengurus BUMDesa", name: "Struktur Organisasi dan SK Pengurus BUMDesa",
href: "/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa" href: "/darmasaba/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes"
}, },
{ {
id: "5.4", id: "5.4",