Compare commits

..

3 Commits

Author SHA1 Message Date
f436aa2ef0 Fix QC Kak Inno Mobile Done
FIx QC Kak Ayu Mobile Admin Done
Fix Tampilan Admin Mobile Device All Menu Done
2026-01-02 16:33:15 +08:00
50bc54ceca Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Menu Inovasi
2025-12-24 14:36:51 +08:00
f0f201c853 Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Mobile Device Menu Ekonomi
Fix Search -> useDebounced Menu Ekonomi
2025-12-23 17:18:36 +08:00
229 changed files with 8250 additions and 2932 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

@@ -68,7 +68,7 @@ const category = proxy({
const res = await ApiFetch.api.desa.kategoripengumuman[
"findMany"
].get({
query: { page, limit },
query: { page, limit, search },
});
if (res.status === 200 && res.data?.success) {

View File

@@ -65,7 +65,7 @@ const potensiDesa = proxy({
const res = await ApiFetch.api.desa.potensi[
"find-many"
].get({
query: { page, limit },
query: { page, limit, search },
});
if (res.status === 200 && res.data?.success) {

View File

@@ -1,7 +1,7 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -31,22 +31,18 @@ function Page() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header + tombol edit */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2} c={colors['blue-button']} lh={1.2} />
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
<Group justify="space-between">
<Title order={2} c={colors['blue-button']} lh={1.2}>Profil Perbekel</Title>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
>
Edit
</Button>
</Group>
{/* Card Profil */}
<Paper p="xl" bg={colors['white-1']} withBorder radius="md" shadow="xs">
@@ -60,7 +56,7 @@ function Page() {
<GridCol span={12}>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
fz={{ base: 'sm', md: 'xl' }}
fw="bold"
c={colors['blue-button']}
lh={{ base: 1.45, md: 1.45 }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,30 @@
'use client'
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
import { usePathname } from 'next/navigation';
import { Box } from '@mantine/core';
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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
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) {
console.error('Error updating pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
@@ -153,12 +153,12 @@ export default function EditPegawaiBumDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai PPID</Title>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai BumDes</Title>
</Group>
<Paper

View File

@@ -1,5 +1,6 @@
'use client'
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 colors from '@/con/colors';
@@ -28,7 +29,7 @@ function DetailPegawai() {
statePegawai.delete.byId(selectedId);
setModalHapus(false);
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);
setModalNonActive(false);
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;
return (
<Box>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
@@ -60,7 +61,7 @@ function DetailPegawai() {
</Box>
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -68,7 +69,7 @@ function DetailPegawai() {
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pegawai PPID
Detail Pegawai BumDes
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
@@ -165,7 +166,7 @@ function DetailPegawai() {
<Button
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"
radius="md"
size="md"
@@ -187,7 +188,7 @@ function DetailPegawai() {
/>
{/* Modal NonActive */}
<ModalKonfirmasiHapus
<ModalKonfirmasiNonAktif
opened={modalNonActive}
onClose={() => setModalNonActive(false)}
onConfirm={handleNonActive}

View File

@@ -72,7 +72,7 @@ function CreatePegawaiBumDes() {
// Reset form dan redirect
resetForm();
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) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
@@ -82,13 +82,13 @@ function CreatePegawaiBumDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Pegawai BUMDesa
Tambah Pegawai BUMDes
</Title>
</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();
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) {
console.error('Error updating posisi organisasi:', err);
@@ -106,7 +106,7 @@ function EditPosisiOrganisasiBumDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -36,7 +36,7 @@ function CreatePosisiOrganisasiBumDes() {
await stateOrganisasi.create.submit();
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) {
toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error);
@@ -46,7 +46,7 @@ function CreatePosisiOrganisasiBumDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */}
<Group mb="md">
<Button

View File

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

View File

@@ -106,38 +106,52 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Demografi Pekerjaan</Title>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
List Demografi Pekerjaan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
fz={{ base: 'sm', md: 'md' }}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ minWidth: 200 }}>Pekerjaan</TableTh>
<TableTh style={{ minWidth: 200 }}>Laki - Laki</TableTh>
<TableTh style={{ minWidth: 200 }}>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
<TableTh style={{ width: '40%' }}>Pekerjaan</TableTh>
<TableTh style={{ width: '20%' }}>Laki - Laki</TableTh>
<TableTh style={{ width: '20%' }}>Perempuan</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ minWidth: 200 }}>{item.pekerjaan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.lakiLaki}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.perempuan}</TableTd>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"
@@ -145,8 +159,11 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
fz="sm"
px="xs"
py="xs"
>
<IconEdit size={18} />
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd>
@@ -158,17 +175,22 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="sm"
px="xs"
py="xs"
>
<IconTrash size={18} />
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<TableTd colSpan={5}>
<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>
</TableTd>
</TableTr>
@@ -176,6 +198,78 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</TableTbody>
</Table>
</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>
{/* Pagination */}
@@ -195,10 +289,13 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</Center>
{/* Chart */}
<Box mt={30} style={{ width: '100%', minHeight: 400 }}>
<Paper bg={colors['white-1']} p="md" radius="md" withBorder>
<Stack gap={"xs"}>
<Title pb={10} order={4}>
<Box mt={{ base: 'lg', md: 'xl' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="md" withBorder>
<Stack gap="xs">
<Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Grafik Demografi Pekerjaan
</Title>
{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}>
<Group justify='center'>
<Flex align="center" gap={10}>
<Box bg="#5082EE" w={20} h={20} />
<Text>Laki - Laki</Text>
<Box py={{ base: 'sm', md: 'md' }}>
<Group justify="center" gap='md'>
<Flex align="center" gap={8}>
<Box bg="#5082EE" w={16} h={16} />
<Text fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Laki - Laki
</Text>
</Flex>
<Flex align="center" gap={10}>
<Box bg="#6EDF9C" w={20} h={20} />
<Text>Perempuan</Text>
<Flex align="center" gap={8}>
<Box bg="#6EDF9C" w={16} h={16} />
<Text fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Perempuan
</Text>
</Flex>
</Group>
</Box>
@@ -242,4 +345,4 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
);
}
export default DemografiPekerjaan;
export default DemografiPekerjaan;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,28 @@
'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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Flex,
Group,
Pagination,
Paper,
Skeleton,
@@ -19,7 +20,7 @@ import {
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -51,6 +52,7 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const handleDelete = async () => {
if (selectedId) {
@@ -64,8 +66,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
useEffect(() => {
if (stategrafik.findMany.data) {
@@ -87,19 +89,21 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
{/* Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Box py={{ base: 'sm', md: 'md' }}>
{/* Table - Desktop */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mb="lg">
<Stack gap="md">
<Flex justify="space-between" align="center" visibleFrom='md'>
<Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Usia Kerja
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -112,26 +116,58 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</Button>
</Flex>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<Group align="center" hiddenFrom='md'>
<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>
<TableTr>
<TableTh>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46+</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '20%' }}>Usia 18-25</TableTh>
<TableTh style={{ width: '20%' }}>Usia 26-35</TableTh>
<TableTh style={{ width: '20%' }}>Usia 36-45</TableTh>
<TableTh style={{ width: '20%' }}>Usia 46+</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia26_35}</TableTd>
<TableTd>{item.usia36_45}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd>
<TableTd fz="md" fw={500} lh={1.5}>
{item.usia18_25}
</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>
<Button
color="green"
@@ -160,7 +196,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
<TableTr>
<TableTd colSpan={6}>
<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>
</TableTd>
</TableTr>
@@ -168,6 +206,80 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</TableTbody>
</Table>
</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>
</Paper>
@@ -189,8 +301,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
{/* Donut Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Title order={3} pb={10}>
<Stack gap="md">
<Title order={4} lh={1.2}>
Grafik Pengangguran Berdasarkan Usia Kerja
</Title>
{donutData.length > 0 ? (
@@ -205,7 +317,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
/>
</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>
</Paper>
@@ -221,4 +335,4 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
);
}
export default GrafikBerdasarkanUsiaKerjaYangMenganggur;
export default GrafikBerdasarkanUsiaKerjaYangMenganggur;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ function DetailLowonganKerjaLokal() {
const data = lowonganState.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -99,12 +99,16 @@ function DetailLowonganKerjaLokal() {
<Box>
<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>
<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>
<Group gap="sm" mt="sm">

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
'use client'
import colors from '@/con/colors';
import {
Box,
ScrollArea,
Stack,
Tabs,
@@ -68,36 +69,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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>
<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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,29 @@
'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}) {
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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -17,7 +17,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -44,6 +44,7 @@ function AjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState)
const router = useRouter()
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,
@@ -54,8 +55,8 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
} = state.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []

View File

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

View File

@@ -49,7 +49,7 @@ function DetailDesaDigital() {
const data = stateDesaDigital.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */}
<Button
variant="subtle"
@@ -63,7 +63,7 @@ function DetailDesaDigital() {
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -77,12 +77,12 @@ function DetailDesaDigital() {
{/* Sub Card Detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Box pl={5}>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Box pl={5}>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -45,28 +45,31 @@ function DesaDigitalSmartVillage() {
function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Desa Digital Smart Village</Title>
<Title order={4} lh={1.2}>
List Desa Digital Smart Village
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -78,15 +81,34 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="sm">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Inovasi</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat Inovasi
<TableTh style={{ width: '30%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Nama Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '50%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -94,21 +116,18 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.name}
</Text>
</TableTd>
<TableTd>
<Box w={200}>
<Text
fz="sm"
c="dimmed"
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
<Text
fz="sm"
c="dimmed"
lh={1.5}
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd>
<TableTd>
<Button
@@ -121,7 +140,9 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</TableTd>
</TableTr>
@@ -129,8 +150,8 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data inovasi digital yang cocok
</Text>
</Center>
@@ -140,6 +161,64 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="sm">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={4}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Inovasi
</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi
</Text>
<Box pl={5}>
<Text
fz="sm"
fw={500}
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</Box>
<Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() =>
router.push(
`/admin/inovasi/desa-digital-smart-village/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data inovasi digital yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination

View File

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

View File

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

View File

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

View File

@@ -16,9 +16,9 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -45,28 +45,31 @@ function InfoTeknologiTepatGuna() {
function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', md: 'lg' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Box py={{ base: 'sm', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Info Teknologi Tepat Guna</Title>
<Title order={4} lh={1.2}>
Daftar Info Teknologi Tepat Guna
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -79,40 +82,57 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>
Nama Info Teknologi
<Text fz="sm" fw={600} lh={1.4}>
Nama Info Teknologi
</Text>
</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '50%' }}>
<TableTd>
<Text
fz="sm"
lh={1.5}
c="dimmed"
lineClamp={1}
truncate
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</TableTd>
<TableTd style={{ width: '20%' }}>
<TableTd>
<Button
size="xs"
radius="md"
@@ -134,7 +154,7 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Text fz="sm" c="dimmed" ta="center">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
@@ -144,6 +164,63 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" 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 Info Teknologi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz={"sm"} fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Box pl={5}>
<Text
fz="sm"
fw={500}
lh={1.4}
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</Box>
</Box>
<Group justify="flex-end">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`,
)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text fz="sm" c="dimmed" ta="center">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
)}
</Stack>
</Paper>
<Center>
@@ -164,4 +241,4 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
);
}
export default InfoTeknologiTepatGuna;
export default InfoTeknologiTepatGuna;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconListDetails, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -57,36 +57,76 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper biar rapi */}
<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",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<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}

View File

@@ -1,7 +1,29 @@
'use client'
import React from 'react';
import LayoutTabsKolaborasi from './_lib/layoutTabs';
import { usePathname } from 'next/navigation';
import { Box } from '@mantine/core';
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 (
<LayoutTabsKolaborasi>
{children}

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -45,69 +46,107 @@ function KolaborasiInovasi() {
function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, loading, page, totalPages, load } = listState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'xs', md: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kolaborasi Inovasi</Title>
<Box py={{ base: 'xs', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={1.2}>
Daftar Kolaborasi Inovasi
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
onClick={() =>
router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')
}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '15%' }}>Tahun</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
<TableTh style={{ width: '5%' }}>
<Text fz="xs" fw={600} lh={1.4} ta="center">
No
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="xs" fw={600} lh={1.4}>
Nama Kolaborasi Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="xs" fw={600} lh={1.4} ta="center">
Tahun
</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>
<Text fz="xs" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz="xs" fw={600} lh={1.4} ta="center">
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
<TableTd ta="center">
<Text fz="sm" fw={500} lh={1.45}>
{index + 1}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
<Text fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd ta="center">
<Text fz="sm" fw={500} lh={1.45}>
{item.tahun}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate="end" lineClamp={1} c="dimmed">
<Text fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.slug}
</Text>
</TableTd>
<TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
@@ -115,7 +154,9 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`)
router.push(
`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`
)
}
>
Detail
@@ -127,7 +168,9 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kolaborasi inovasi yang tersedia</Text>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kolaborasi inovasi yang tersedia
</Text>
</Center>
</TableTd>
</TableTr>
@@ -135,23 +178,95 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<Paper key={item.id} withBorder radius="md" p="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Kolaborasi Inovasi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.slug}
</Text>
</Box>
<Box>
<Button
fullWidth
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`
)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kolaborasi inovasi yang tersedia
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt={{ base: 'sm', md: 'md' }}
mb={{ base: 'sm', md: 'md' }}
color="blue"
radius="md"
/>
</Center>
)}
</Box>
);
}
export default KolaborasiInovasi;
export default KolaborasiInovasi;

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -27,6 +27,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { useDebouncedValue } from '@mantine/hooks';
function MitraKolaborasi() {
const [search, setSearch] = useState('');
@@ -59,27 +60,34 @@ function ListMitraKolaborasi({ search }: { search: string }) {
}
};
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, loading, page, totalPages, load } = listState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Mitra Kolaborasi</Title>
<Group justify="space-between" mb="lg">
<Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Daftar Mitra Kolaborasi
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -93,41 +101,82 @@ function ListMitraKolaborasi({ search }: { search: string }) {
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh style={{ width: '30%' }}>Nama Mitra</TableTh>
<TableTh style={{ width: '25%' }}>Image</TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh>
<TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
No
</Text>
</TableTh>
<TableTh style={{ width: '30%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Nama Mitra
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Image
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Delete
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Edit
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
<Text fz={{ base: 'xs', md: 'sm' }} fw={500} lh={1.5}>
{index + 1}
</Text>
</TableTd>
<TableTd>
<Text
fz={{ base: 'sm', md: 'md' }}
fw={500}
lh={1.5}
truncate="end"
lineClamp={1}
>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box
w={70}
h={70}
>
<Box w={70} h={70}>
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
radius="sm"
/>
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
<Box
bg={colors['blue-button']}
w="100%"
h="100%"
/>
)}
</Box>
</TableTd>
@@ -167,8 +216,8 @@ function ListMitraKolaborasi({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" ta="center">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
@@ -178,7 +227,96 @@ function ListMitraKolaborasi({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Mitra
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Image
</Text>
<Box w="100%" h={60} mt="xs">
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
radius="sm"
h="100%"
/>
) : (
<Box
bg={colors['blue-button']}
w="100%"
h="100%"
/>
)}
</Box>
</Box>
<Group justify="flex-end" mt="md" gap="xs">
<Button
size="xs"
radius="md"
variant="light"
color="red"
disabled={mitraKolaborasi.delete.loading || !item}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={16} />
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="green"
disabled={!item}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" ta="center">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
@@ -187,14 +325,13 @@ function ListMitraKolaborasi({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
mt="lg"
mb="lg"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
@@ -205,4 +342,4 @@ function ListMitraKolaborasi({ search }: { search: string }) {
);
}
export default MitraKolaborasi;
export default MitraKolaborasi;

View File

@@ -2,6 +2,7 @@
'use client'
import colors from '@/con/colors';
import {
Box,
ScrollArea,
Stack,
Tabs,
@@ -84,36 +85,76 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }
keepMounted={false}
>
{/* ✅ Scroll horizontal biar gak overflow */}
<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",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<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}

View File

@@ -42,8 +42,8 @@ function DetailAdministrasiOnline() {
const data = stateAdminOnline.findUnique.data;
return (
<Box py={10}>
<Group justify='space-between' align='center' w={{ base: "100%", md: "50%" }} mb={10}>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group justify='space-between' align='center' w={{ base: "100%", md: "70%" }} mb={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"

View File

@@ -17,7 +17,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -44,36 +44,55 @@ function AdministrasiOnline() {
function ListAdministrasiOnline({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.administrasiOnline);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4}>Daftar Administrasi Online</Title>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title order={4} lh={1.2} mb={{ base: 'md', md: 'lg' }}>
Daftar Administrasi Online
</Title>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Alamat</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Alamat</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Aksi</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -81,17 +100,17 @@ function ListAdministrasiOnline({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lineClamp={1}>
<Text fz="sm" c="gray.7" lh={1.5} lineClamp={1}>
{item.alamat}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" lineClamp={1}>
<Text fz="sm" c="gray.7" lh={1.5} lineClamp={1}>
{item.nomorTelepon || '-'}
</Text>
</TableTd>
@@ -116,8 +135,10 @@ function ListAdministrasiOnline({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data administrasi online yang cocok</Text>
<Center py={24}>
<Text c="gray.6" fz="sm" lh={1.4}>
Tidak ada data administrasi online yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -125,6 +146,55 @@ function ListAdministrasiOnline({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} p="md" withBorder>
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.5}>{item.name}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Alamat</Text>
<Text fz="sm" fw={500} c="gray.7" lh={1.5}>{item.alamat}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
<Text fz="sm" fw={500} c="gray.7" lh={1.5}>{item.nomorTelepon || '-'}</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/administrasi-online/${item.id}`
)
}
fullWidth
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="gray.6" fz="sm" lh={1.4}>
Tidak ada data administrasi online yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>

View File

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

View File

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

View File

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

View File

@@ -16,9 +16,10 @@ import {
TableThead,
TableTr,
Text,
Title
Title,
useMantineTheme
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -45,28 +46,35 @@ function JenisLayanan() {
function ListJenisLayanan({ search }: { search: string }) {
const stateList = useProxy(layananonlineDesa.jenisLayanan);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = stateList.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
const theme = useMantineTheme();
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="lg">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Stack py="lg" gap="xl">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jenis Layanan</Title>
<Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Daftar Jenis Layanan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -80,13 +88,34 @@ function ListJenisLayanan({ search }: { search: string }) {
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Jenis Layanan</TableTh>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '30%' }}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Nama Jenis Layanan
</Text>
</TableTh>
<TableTh style={{ width: '55%' }}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Deskripsi
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center" c={theme.black}>
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -94,16 +123,29 @@ function ListJenisLayanan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
<Text
fz="md"
fw={500}
lh={1.5}
c={theme.black}
truncate="end"
lineClamp={1}
>
{item.nama}
</Text>
</TableTd>
<TableTd style={{ width: '40%' }}>
<Text fz="sm" c="dimmed" lineClamp={2}>
<TableTd style={{ width: '55%' }}>
<Text
fz="sm"
fw={500}
lh={1.5}
c={theme.black}
lineClamp={2}
>
{item.deskripsi || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<TableTd style={{ width: '15%' }} ta="center">
<Button
size="xs"
radius="md"
@@ -124,8 +166,8 @@ function ListJenisLayanan({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jenis layanan yang cocok
</Text>
</Center>
@@ -135,22 +177,76 @@ function ListJenisLayanan({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder radius="md" p="md">
<Stack gap="xs">
<Box pl={5}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Nama Jenis Layanan
</Text>
<Text fz="sm" fw={500} lh={1.5} c={theme.black}>
{item.nama}
</Text>
</Box>
<Box pl={5}>
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Deskripsi
</Text>
<Text fz="sm" fw={500} lh={1.5} c={theme.black}>
{item.deskripsi || '-'}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`
)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada jenis layanan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
color="blue"
radius="md"
/>
</Center>
)}
</Stack>
);
}

View File

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

View File

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

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