nico/23-des-25 #45
36
src/app/admin/(dashboard)/_com/modalNonaktif.tsx
Normal file
36
src/app/admin/(dashboard)/_com/modalNonaktif.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,30 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabsBerita from './_com/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 (
|
||||
<LayoutTabsBerita>
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
'use client'
|
||||
import { usePathname } from "next/navigation";
|
||||
import LayoutTabsGallery 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 (
|
||||
<LayoutTabsGallery>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
'use client'
|
||||
import { usePathname } from "next/navigation";
|
||||
import LayoutTabsLayanan from "../_com/layoutTabLayanan";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
export default function Layout({children} : {children: React.ReactNode}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Contoh path:
|
||||
// - /darmasaba/desa/layanan/semua → panjang 5 → list
|
||||
// - /darmasaba/desa/layanan/Pemerintahan → panjang 5 → list
|
||||
// - /darmasaba/desa/layanan/Pemerintahan/123 → panjang 6 → detail
|
||||
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length >= 5;
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<LayoutTabsLayanan>
|
||||
<Box>
|
||||
{children}
|
||||
</LayoutTabsLayanan>
|
||||
)
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabsLayanan>
|
||||
{children}
|
||||
</LayoutTabsLayanan>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,29 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabs from './_com/layoutTabs';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Contoh path:
|
||||
// - /darmasaba/desa/pengumuman/semua → panjang 5 → list
|
||||
// - /darmasaba/desa/pengumuman/Pemerintahan → panjang 5 → list
|
||||
// - /darmasaba/desa/pengumuman/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}
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabsPotensi 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 (
|
||||
<LayoutTabsPotensi>
|
||||
{children}
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import LayoutTabsDetail from "./_lib/layoutTabsDetail"
|
||||
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 (
|
||||
<LayoutTabsDetail>
|
||||
{children}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
@@ -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} />
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={4}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Judul Program
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi Singkat
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4} c="dark.9" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jumlah Masyarakat Miskin
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.statistik?.jumlah || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
|
||||
fz="sm"
|
||||
lh={1.4}
|
||||
>
|
||||
<IconDeviceImac size={18} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data program kemiskinan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,12 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import LayoutTabs from "./_lib/layoutTabs"
|
||||
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LayoutTabs>
|
||||
{children}
|
||||
</LayoutTabs>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -142,7 +142,7 @@ function EditKeamananLingkungan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -44,7 +44,7 @@ function DetailKeamananLingkungan() {
|
||||
const data = keamananState.findUnique.data
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -58,7 +58,7 @@ function DetailKeamananLingkungan() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -88,7 +88,9 @@ function DetailKeamananLingkungan() {
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
<Box pl={8}>
|
||||
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Aksi */}
|
||||
|
||||
@@ -78,7 +78,7 @@ function CreateKeamananLingkungan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -48,6 +48,7 @@ function KeamananLingkungan() {
|
||||
function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
const keamananState = useProxy(keamananLingkunganState)
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000)
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,25 +59,27 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
} = keamananState.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">
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Keamanan Lingkungan</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Keamanan Lingkungan
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -89,14 +92,18 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover
|
||||
miw={0}
|
||||
style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: 140, textAlign: 'center' }}>
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -104,25 +111,32 @@ function ListKeamananLingkungan({ 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" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" c="dimmed" truncate lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<TableTd style={{ overflow: 'hidden' }}>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
lh={1.5}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ width: 140, textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
|
||||
leftSection={<IconDeviceImacCog size={20} />}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -130,8 +144,8 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Center py={{ base: 'sm', md: 'lg' }}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data keamanan lingkungan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -141,6 +155,49 @@ function ListKeamananLingkungan({ 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="sm">
|
||||
<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}>Deskripsi</Text>
|
||||
<Box pl={8}>
|
||||
<Text fz="sm" lineClamp={3} style={{ wordBreak: 'break-word' }} fw={500} lh={1.5} c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml={5} fz="sm" fw={500} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data keamanan lingkungan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -152,8 +209,8 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
mt={{ base: 'sm', md: 'md' }}
|
||||
mb={{ base: 'sm', md: 'md' }}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
@@ -162,4 +219,4 @@ function ListKeamananLingkungan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KeamananLingkungan;
|
||||
export default KeamananLingkungan;
|
||||
@@ -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 { IconPhone, IconTag } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -57,35 +57,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", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
w="100%"
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
<TabsList
|
||||
p="xs" // lebih kecil
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
width: "max-content", // ⬅️ kunci
|
||||
maxWidth: "100%", // ⬅️ penting
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -107,7 +107,7 @@ function EditKontakItem() {
|
||||
};
|
||||
|
||||
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">
|
||||
|
||||
@@ -41,7 +41,7 @@ function DetailKontakDarurat() {
|
||||
const data = kontakState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -55,7 +55,7 @@ function DetailKontakDarurat() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -46,7 +46,7 @@ function CreateKontakItem() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -26,7 +26,6 @@ import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import kontakDarurat from '../../../_state/keamanan/kontak-darurat-keamanan';
|
||||
|
||||
|
||||
function KontakItem() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
@@ -49,6 +48,7 @@ function KontakItem() {
|
||||
function ListKontakItem({ search }: { search: string }) {
|
||||
const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -59,43 +59,54 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
} = kontakState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ 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">
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Kontak Darurat Item</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Kontak Darurat Item
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item/create')}
|
||||
onClick={() =>
|
||||
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop: Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kontak</TableTh>
|
||||
<TableTh>Nomor Telepon</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: '45%' }}>Nama Kontak</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Nomor Telepon</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -103,12 +114,12 @@ function ListKontakItem({ 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.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="dimmed" lh={1.5}>
|
||||
{item.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -116,19 +127,26 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`
|
||||
)
|
||||
}
|
||||
size="xs"
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<IconDeviceImacCog size={16} />
|
||||
<Text ml={4} fz="sm" fw={500}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat item yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -138,6 +156,58 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile: Card */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} p="sm" withBorder radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama Kontak
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nomor Telepon
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4} c="dimmed">
|
||||
{item.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="xs"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconDeviceImacCog size={16} />
|
||||
<Text ml={4} fz="sm" fw={500}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat item yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -159,4 +229,4 @@ function ListKontakItem({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KontakItem;
|
||||
export default KontakItem;
|
||||
@@ -147,7 +147,7 @@ export default function EditKontakDaruratKeamanan() {
|
||||
}));
|
||||
|
||||
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} />
|
||||
|
||||
@@ -42,7 +42,7 @@ function DetailKontakDaruratKeamanan() {
|
||||
const data = kontakState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -56,7 +56,7 @@ function DetailKontakDaruratKeamanan() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -53,7 +53,7 @@ function CreateKontakDaruratKeamanan() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -48,6 +48,7 @@ function KontakDaruratKeamanan() {
|
||||
function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,26 +59,28 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
} = kontakState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
load(page, 10, debouncedSearch);
|
||||
kontakDarurat.kontakDaruratItem.findMany.load();
|
||||
}, [page, search]);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Kontak Darurat Keamanan</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Kontak Darurat Keamanan
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -88,15 +91,22 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Kontak Darurat</TableTh>
|
||||
<TableTh>Nama Kontak</TableTh>
|
||||
<TableTh>Nomor Telepon</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Kontak Darurat</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nama Kontak</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nomor Telepon</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -104,17 +114,17 @@ function ListKontakDaruratKeamanan({ 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 fz="sm" c="dimmed" truncate lineClamp={1}>
|
||||
<Text fz="sm" c="dimmed" lh={1.5} truncate lineClamp={1}>
|
||||
{item.kategori?.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="dimmed" lh={1.5}>
|
||||
{item.kategori?.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -125,7 +135,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -134,7 +144,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat keamanan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -144,6 +154,55 @@ function ListKontakDaruratKeamanan({ 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 radius="md" p="md">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kontak Darurat</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama Kontak</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{item.kategori?.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nomor Telepon</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed">
|
||||
{item.kategori?.nomorTelepon || "-"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kontak darurat keamanan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -165,4 +224,4 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default KontakDaruratKeamanan;
|
||||
export default KontakDaruratKeamanan;
|
||||
@@ -1,8 +1,29 @@
|
||||
'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}
|
||||
|
||||
@@ -127,7 +127,7 @@ function EditLaporanPublik() {
|
||||
};
|
||||
|
||||
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">
|
||||
|
||||
@@ -48,7 +48,7 @@ function DetailLaporanPublik() {
|
||||
const data = stateLaporan.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -61,7 +61,7 @@ function DetailLaporanPublik() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -50,7 +50,7 @@ function CreateLaporanPublik() {
|
||||
};
|
||||
|
||||
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 variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -45,6 +45,7 @@ function LaporanPublik() {
|
||||
function ListLaporanPublik({ search }: { search: string }) {
|
||||
const stateLaporan = useProxy(laporanPublikState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -55,42 +56,54 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
} = stateLaporan.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Laporan Publik</Title>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Laporan Publik
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
|
||||
visibleFrom="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={{ width: '25%' }}>Judul Laporan Publik</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Status</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Judul Laporan Publik</Text></TableTh>
|
||||
<TableTh style={{ width: '20%' }}><Text fz="sm" fw={600} lh={1.4}>Tanggal</Text></TableTh>
|
||||
<TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Status</Text></TableTh>
|
||||
<TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text></TableTh>
|
||||
<TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -98,12 +111,12 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
<Text fw={500} fz="md" lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="gray.7" lh={1.5}>
|
||||
{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -131,9 +144,7 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
|
||||
</Box>
|
||||
<Text truncate fz="sm" c="gray.7" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -144,7 +155,7 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -153,7 +164,7 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Text c="gray.6" fz="sm" lh={1.5}>
|
||||
Tidak ada data laporan publik yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -163,7 +174,88 @@ function ListLaporanPublik({ 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}>Judul Laporan Publik</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5}>{item.judul}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Tanggal</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="gray.7">
|
||||
{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
|
||||
<Box
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '16px',
|
||||
backgroundColor:
|
||||
item.status === 'Selesai' ? '#94EF95FF' :
|
||||
item.status === 'Proses' ? '#F1D295FF' :
|
||||
'#F38E8EFF',
|
||||
color:
|
||||
item.status === 'Selesai' ? '#01BA01FF' :
|
||||
item.status === 'Proses' ? '#B67A00FF' :
|
||||
'#AE1700FF',
|
||||
fontWeight: 900,
|
||||
fontSize: '0.75rem',
|
||||
textAlign: 'center',
|
||||
minWidth: '80px',
|
||||
}}
|
||||
>
|
||||
{item.status}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="gray.7" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
router.push(`/admin/keamanan/laporan-publik/${item.id}`)
|
||||
}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5} fz="sm" fw={500}>Detail</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="gray.6" fz="sm" lh={1.5}>
|
||||
Tidak ada data laporan publik yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Tambah Baru (Mobile only) */}
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
|
||||
hiddenFrom="md"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -182,4 +274,4 @@ function ListLaporanPublik({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default LaporanPublik;
|
||||
export default LaporanPublik;
|
||||
@@ -120,7 +120,7 @@ function EditPencegahanKriminalitas() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Back button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -40,7 +40,7 @@ function DetailPencegahanKriminalitas() {
|
||||
const data = kriminalitasState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -54,7 +54,7 @@ function DetailPencegahanKriminalitas() {
|
||||
{/* Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -59,7 +59,7 @@ function CreatePencegahanKriminalitas() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -46,8 +46,9 @@ function PencegahanKriminalitas() {
|
||||
}
|
||||
|
||||
function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
const kriminalitasState = useProxy(pencegahanKriminalitasState)
|
||||
const kriminalitasState = useProxy(pencegahanKriminalitasState);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -58,24 +59,28 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
} = kriminalitasState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
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">
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Pencegahan Kriminalitas</Title>
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title
|
||||
order={4}
|
||||
lh={{ base: 1.2, md: 1.1 }}
|
||||
>
|
||||
Daftar Pencegahan Kriminalitas
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -86,15 +91,22 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Tabel */}
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Pencegahan</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Deskripsi Singkat</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nama Pencegahan</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Deskripsi Singkat</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -102,35 +114,29 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
data.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
truncate
|
||||
lineClamp={1}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz="sm"
|
||||
lh={1.45}
|
||||
truncate
|
||||
lineClamp={1}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
truncate
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.deskripsiSingkat || ''
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.45}
|
||||
truncate
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.deskripsiSingkat || ''
|
||||
}}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -139,7 +145,9 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
<Text ml={5} fz="sm" fw={500} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -148,7 +156,7 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data pencegahan kriminalitas yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
@@ -158,6 +166,71 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{data.length > 0 ? (
|
||||
data.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama Pencegahan
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi Singkat
|
||||
</Text>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }}
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.4}
|
||||
lineClamp={3}
|
||||
ta="justify"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.4}
|
||||
lineClamp={3}
|
||||
ta="justify"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5} fz="sm" fw={500} lh={1.4}>
|
||||
Detail
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data pencegahan kriminalitas yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -175,8 +248,8 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default PencegahanKriminalitas;
|
||||
export default PencegahanKriminalitas;
|
||||
@@ -247,7 +247,7 @@ function EditPolsekTerdekat() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={modalOpen}
|
||||
|
||||
@@ -40,7 +40,7 @@ function DetailPolsekTerdekat() {
|
||||
const data = polsekState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Tombol Back */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -54,7 +54,7 @@ function DetailPolsekTerdekat() {
|
||||
{/* Wrapper Detail */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -148,7 +148,7 @@ function CreatePolsekTerdekat() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Modal Tambah Layanan */}
|
||||
<Modal
|
||||
opened={modalOpen}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user