Compare commits
2 Commits
nico/15-de
...
nico/17-de
| Author | SHA1 | Date | |
|---|---|---|---|
| dc8793e3ae | |||
| c8484357cb |
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -136,12 +137,43 @@ const statepermohonanInformasiPublik = proxy({
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
statepermohonanInformasiPublik.findMany.loading = true; // Use the full path to access the property
|
||||
statepermohonanInformasiPublik.findMany.page = page;
|
||||
statepermohonanInformasiPublik.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
statepermohonanInformasiPublik.findMany.data = res.data.data || [];
|
||||
statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
|
||||
statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||
statepermohonanInformasiPublik.findMany.data = [];
|
||||
statepermohonanInformasiPublik.findMany.total = 0;
|
||||
statepermohonanInformasiPublik.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading permohonan keberatan informasi:", error);
|
||||
statepermohonanInformasiPublik.findMany.data = [];
|
||||
statepermohonanInformasiPublik.findMany.total = 0;
|
||||
statepermohonanInformasiPublik.findMany.totalPages = 1;
|
||||
} finally {
|
||||
statepermohonanInformasiPublik.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -57,17 +58,48 @@ const permohonanKeberatanInformasi = proxy({
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
permohonanKeberatanInformasi.findMany.loading = true; // Use the full path to access the property
|
||||
permohonanKeberatanInformasi.findMany.page = page;
|
||||
permohonanKeberatanInformasi.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
permohonanKeberatanInformasi.findMany.data = res.data.data || [];
|
||||
permohonanKeberatanInformasi.findMany.total = res.data.total || 0;
|
||||
permohonanKeberatanInformasi.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||
permohonanKeberatanInformasi.findMany.data = [];
|
||||
permohonanKeberatanInformasi.findMany.total = 0;
|
||||
permohonanKeberatanInformasi.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading permohonan keberatan informasi:", error);
|
||||
permohonanKeberatanInformasi.findMany.data = [];
|
||||
permohonanKeberatanInformasi.findMany.total = 0;
|
||||
permohonanKeberatanInformasi.findMany.totalPages = 1;
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -108,7 +108,7 @@ function DetailPerbekelDariMasa() {
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconX size={20} />
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function EditKolaborasiInovasi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -42,7 +42,7 @@ function DetailSDGSDesa() {
|
||||
const data = sdgsState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -54,7 +54,7 @@ function DetailSDGSDesa() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -106,7 +106,7 @@ function DetailSDGSDesa() {
|
||||
size="md"
|
||||
disabled={sdgsState.delete.loading}
|
||||
>
|
||||
<IconX size={20} />
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -65,7 +65,7 @@ function CreateSDGsDesa() {
|
||||
}
|
||||
}
|
||||
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} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -28,6 +28,8 @@ function SdgsDesa() {
|
||||
function ListSdgsDesa({ search }: { search: string }) {
|
||||
const listState = useProxy(sdgsDesa);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -38,8 +40,8 @@ function ListSdgsDesa({ search }: { search: string }) {
|
||||
} = listState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
@@ -63,7 +65,7 @@ function ListSdgsDesa({ search }: { search: string }) {
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color={colors['blue-button']}
|
||||
color='blue'
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/SDGs/create')}
|
||||
>
|
||||
@@ -147,12 +149,18 @@ function ListSdgsDesa({ search }: { search: string }) {
|
||||
{filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="md">
|
||||
<Stack gap={4}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text fz="xs" c="dark.6" lh={1.4}>
|
||||
Jumlah: {item.jumlah || '0'}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama SDGs Desa</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Jumlah</Text>
|
||||
<Text fz="xs" c="dark.6" lh={1.4}>
|
||||
{item.jumlah || '0'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -204,7 +204,7 @@ function EditAPBDes() {
|
||||
};
|
||||
|
||||
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} />
|
||||
@@ -215,7 +215,7 @@ function EditAPBDes() {
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '100%' }}
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -368,6 +368,13 @@ function EditAPBDes() {
|
||||
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
|
||||
{ value: '3', label: 'Level 3 (Detail)' },
|
||||
]}
|
||||
styles={{
|
||||
option: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
value={String(newItem.level)}
|
||||
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
|
||||
/>
|
||||
@@ -378,6 +385,13 @@ function EditAPBDes() {
|
||||
{ value: 'belanja', label: 'Belanja' },
|
||||
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
||||
]}
|
||||
styles={{
|
||||
option: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
value={newItem.tipe}
|
||||
onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })}
|
||||
/>
|
||||
|
||||
@@ -65,7 +65,7 @@ function DetailAPBDes() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -77,7 +77,7 @@ function DetailAPBDes() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '100%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -155,7 +155,7 @@ function CreateAPBDes() {
|
||||
};
|
||||
|
||||
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} />
|
||||
@@ -353,6 +353,13 @@ function CreateAPBDes() {
|
||||
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
|
||||
{ value: '3', label: 'Level 3 (Detail)' },
|
||||
]}
|
||||
styles={{
|
||||
option: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
value={String(newItem.level)}
|
||||
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
|
||||
/>
|
||||
@@ -363,6 +370,13 @@ function CreateAPBDes() {
|
||||
{ value: 'belanja', label: 'Belanja' },
|
||||
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
||||
]}
|
||||
styles={{
|
||||
option: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
value={newItem.level === 1 ? null : newItem.tipe}
|
||||
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
||||
disabled={newItem.level === 1}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -45,82 +45,97 @@ function APBDes() {
|
||||
function ListAPBDes({ search }: { search: string }) {
|
||||
const listState = useProxy(apbdes);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = listState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar APBDes</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={2} size="lg" lh={1.2}>
|
||||
Daftar APBDes
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>APBDes</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Tahun</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Text fw={500} lineClamp={1}>
|
||||
APBDes {item.tahun}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Text fw={500}>{item.tahun || '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
{item.file?.link ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={item.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
leftSection={<IconFile size={16} />}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
>
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm">
|
||||
Tidak ada dokumen
|
||||
<Box>
|
||||
<Table highlightOnHover miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||
APBDes
|
||||
</TableTh>
|
||||
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||
Tahun
|
||||
</TableTh>
|
||||
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||
Dokumen
|
||||
</TableTh>
|
||||
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
|
||||
APBDes {item.tahun}
|
||||
</Text>
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Box w={100}>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.5}>
|
||||
{item.tahun || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{item.file?.link ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={item.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
leftSection={<IconFile size={16} />}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
fz="sm"
|
||||
>
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ada dokumen
|
||||
</Text>
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
@@ -128,29 +143,126 @@ function ListAPBDes({ search }: { search: string }) {
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={14} />}
|
||||
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||
fullWidth
|
||||
fz="sm"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ada data APBDes yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Center mt="md">
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={2} size="lg" lh={1.2}>
|
||||
Daftar APBDes
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
withBorder
|
||||
bg={colors['white-1']}
|
||||
p="md"
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
APBDes {item.tahun}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text fz="sm"fw={600} lh={1.4}>
|
||||
Tahun
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.tahun || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="sm"fw={600} lh={1.4}>
|
||||
Dokumen
|
||||
</Text>
|
||||
{item.file?.link ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={item.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
leftSection={<IconFile size={14} />}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
fz="xs"
|
||||
lh={1.4}
|
||||
>
|
||||
Lihat
|
||||
</Button>
|
||||
) : (
|
||||
<Text fz="xs" c="dimmed" lh={1.4}>
|
||||
Tidak ada
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={14} />}
|
||||
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||
mt="sm"
|
||||
fz="xs"
|
||||
lh={1.4}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Paper withBorder bg={colors['white-1']} p="md" radius="md">
|
||||
<Center py="lg">
|
||||
<Text c="dimmed" fz="xs" lh={1.4}>
|
||||
Tidak ada data APBDes yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
|
||||
@@ -69,7 +69,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<Box visibleFrom='md'>
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
@@ -102,7 +102,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md'>
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
|
||||
@@ -151,7 +151,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd w={60}>
|
||||
<TableTd w={120}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
@@ -161,7 +161,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd w={60}>
|
||||
<TableTd w={120}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
@@ -191,7 +191,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'xl', md: 'xl' }}>
|
||||
<Box py={{ base: 20, md: 20 }}>
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
|
||||
<Title order={2} lh={1.2}>
|
||||
|
||||
@@ -150,12 +150,18 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} p="sm" radius="md" withBorder shadow="xs">
|
||||
<Stack gap="xs">
|
||||
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
|
||||
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
|
||||
{item.name || '-'}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
|
||||
Kategori: {item.kategori?.name || '-'}
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
|
||||
{item.kategori?.name || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -56,6 +56,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
@@ -63,6 +64,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
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) => (
|
||||
@@ -74,6 +79,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
|
||||
@@ -78,7 +78,7 @@ function EditKategoriPrestasi() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -40,7 +40,7 @@ function CreateKategoriPrestasi() {
|
||||
}
|
||||
|
||||
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} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -32,6 +32,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
@@ -50,14 +51,14 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
} = stateKategori.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 h={500} />
|
||||
</Stack>
|
||||
)
|
||||
@@ -65,28 +66,33 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* DESKTOP: Table */}
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4} c="dark">List Kategori Prestasi</Title>
|
||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Group justify="space-between" mb="xl">
|
||||
<Title order={2} size="lg" lh={1.2}>List Kategori Prestasi</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Box visibleFrom="md">
|
||||
<Table verticalSpacing="sm" highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kategori</TableTh>
|
||||
<TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
|
||||
<TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
|
||||
<TableTh><Text fz="sm" fw={600} c="dark">Nama Kategori</Text></TableTh>
|
||||
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Edit</Text></TableTh>
|
||||
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Delete</Text></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableTr>
|
||||
<TableTd colSpan={2} style={{ textAlign: 'center' }}>
|
||||
<Text py="md" c="dimmed">
|
||||
<TableTd colSpan={3} ta="center">
|
||||
<Text py="md" c="dimmed" fz="sm" lh={1.5}>
|
||||
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
@@ -95,68 +101,130 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
<Text truncate="end" fz="md" lh={1.5} c="dark">
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
<TableTd ta="center" w={120}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</Button>
|
||||
<TableTd ta="center" w={120}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
size="sm"
|
||||
styles={{
|
||||
control: {
|
||||
'&[data-active]': {
|
||||
background: `${colors['blue-button']} !important`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
size="sm"
|
||||
styles={{
|
||||
control: {
|
||||
'&[data-active]': {
|
||||
background: `${colors['blue-button']} !important`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
{/* MOBILE: Card */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="md">
|
||||
{filteredData.length === 0 ? (
|
||||
<Paper p="lg" ta="center">
|
||||
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||
</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} p="md" withBorder bg={colors['white-1']}>
|
||||
<Stack gap="xs">
|
||||
<Text fz="sm" lh={1.5} fw={600} c="dark">{item.name}</Text>
|
||||
<Group justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="xs"
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center py="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
size="xs"
|
||||
styles={{
|
||||
control: {
|
||||
'&[data-active]': {
|
||||
background: `${colors['blue-button']} !important`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
||||
/>
|
||||
</Paper>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
||||
/>
|
||||
</Box >
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriPrestasiDesa
|
||||
export default KategoriPrestasiDesa
|
||||
@@ -128,7 +128,7 @@ export default function EditPrestasiDesa() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -41,7 +41,7 @@ function DetailPrestasiDesa() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -53,7 +53,7 @@ function DetailPrestasiDesa() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -69,7 +69,7 @@ function CreatePrestasiDesa() {
|
||||
}
|
||||
}
|
||||
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} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'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 { useDebouncedValue, useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -28,6 +28,8 @@ function ListPrestasiDesa() {
|
||||
function ListPrestasi({ search }: { search: string }) {
|
||||
const listState = useProxy(prestasiState.prestasiDesa)
|
||||
const router = useRouter();
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -38,93 +40,149 @@ function ListPrestasi({ search }: { search: string }) {
|
||||
} = listState.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 Prestasi Desa</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={2} size={isMobile ? 'md' : 'lg'} lh={1.2}>
|
||||
Daftar Prestasi Desa
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
||||
size={isMobile ? 'xs' : 'sm'}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover striped verticalSpacing="sm" miw={800}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
<TableTh>Nama Prestasi</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Kategori</TableTh>
|
||||
<TableTh ta="center">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Text truncate="end" fz="md" lh={1.5}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Box w={150}>
|
||||
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
|
||||
</Box>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz="md" lh={1.5}>
|
||||
{item.kategori?.name || 'Tidak ada kategori'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%', textAlign: 'center' }}>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<TableTd ta="center">
|
||||
<Center>
|
||||
<Button
|
||||
size="sm"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4} style={{ textAlign: 'center' }}>
|
||||
<Text c="dimmed" py="md">Tidak ada data prestasi</Text>
|
||||
<TableTd colSpan={4} ta="center">
|
||||
<Text c="dimmed" py="md" fz="sm" lh={1.4}>
|
||||
Tidak ada data prestasi
|
||||
</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<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}>Nama Prestasi</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4} lineClamp={2}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.kategori?.name || 'Tidak ada kategori'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={14} />}
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="md">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data prestasi
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="lg">
|
||||
<Center mt={{ base: 'md', md: 'lg' }}>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={load}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
size="sm"
|
||||
size={isMobile ? 'xs' : 'sm'}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
@@ -132,4 +190,4 @@ function ListPrestasi({ search }: { search: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default ListPrestasiDesa;
|
||||
export default ListPrestasiDesa;
|
||||
@@ -75,7 +75,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<Box visibleFrom='md'>
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
@@ -108,7 +108,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md'>
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
|
||||
@@ -20,7 +20,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 MediaSosial() {
|
||||
function ListMediaSosial({ search }: { search: string }) {
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
const getIconSource = (item: any) => {
|
||||
if (item.image?.link) return item.image.link;
|
||||
@@ -66,8 +67,8 @@ function ListMediaSosial({ search }: { search: string }) {
|
||||
} = stateMediaSosial.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
@@ -195,12 +196,15 @@ function ListMediaSosial({ search }: { search: string }) {
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Group justify="space-between" wrap="nowrap" align='center'>
|
||||
<Box>
|
||||
<Text fw={600} fz="sm" lh={1.45}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama Media Sosial / Kontak</Text>
|
||||
<Text fw={500} fz="sm" lh={1.45}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Gambar</Text>
|
||||
</Box>
|
||||
<Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}>
|
||||
{(() => {
|
||||
const src = getIconSource(item);
|
||||
@@ -217,8 +221,8 @@ function ListMediaSosial({ search }: { search: string }) {
|
||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
||||
})()}
|
||||
</Box>
|
||||
</Group>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Link / No. Telepon</Text>
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } 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';
|
||||
@@ -29,12 +29,13 @@ function ProgramInovasi() {
|
||||
function ListProgramInovasi({ search }: { search: string }) {
|
||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
@@ -135,15 +136,22 @@ function ListProgramInovasi({ search }: { search: string }) {
|
||||
>
|
||||
<Stack gap={6}>
|
||||
{/* Title */}
|
||||
<Text fw={600}>{item.name}</Text>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
|
||||
<Text fw={500} lh={1.4}>{item.name}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Description */}
|
||||
<Text fz="sm" c="gray.7" lineClamp={2}>
|
||||
{item.description || '-'}
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" c="gray.7" lineClamp={2}>
|
||||
{item.description || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Link */}
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Link</Text>
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
|
||||
@@ -79,7 +79,7 @@ function EditDaftarInformasiPublik() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -52,7 +52,7 @@ function DetailDaftarInformasiPublik() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
@@ -83,39 +83,39 @@ function DetailDaftarInformasiPublik() {
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
|
||||
<Box
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
<Text
|
||||
px={"xs"}
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
className="prose max-w-none"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
leftSection={<IconEdit size={18} />}
|
||||
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
|
||||
disabled={!data}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
leftSection={<IconEdit size={18} />}
|
||||
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
|
||||
disabled={!data}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={18} />}
|
||||
onClick={() => {
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={stateDaftarInformasi.delete.loading || !data}
|
||||
loading={stateDaftarInformasi.delete.loading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={18} />}
|
||||
onClick={() => {
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={stateDaftarInformasi.delete.loading || !data}
|
||||
loading={stateDaftarInformasi.delete.loading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function CreateDaftarInformasi() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -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, useViewportSize } 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, useViewportSize } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -10,7 +27,7 @@ import HeaderSearch from '../../_com/header';
|
||||
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
|
||||
|
||||
function DaftarInformasiPublik() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
@@ -26,102 +43,158 @@ function DaftarInformasiPublik() {
|
||||
}
|
||||
|
||||
function ListDaftarInformasi({ search }: { search: string }) {
|
||||
const listData = useProxy(daftarInformasiPublik)
|
||||
const router = useRouter()
|
||||
const listData = useProxy(daftarInformasiPublik);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = listData.findMany
|
||||
const { width } = useViewportSize()
|
||||
const isMobile = width < 768
|
||||
const { data, page, totalPages, loading, load } = listData.findMany;
|
||||
const { width } = useViewportSize();
|
||||
const isMobile = width < 768;
|
||||
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>List Daftar Informasi Publik</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
|
||||
>
|
||||
{isMobile ? 'Tambah' : 'Tambah Baru'}
|
||||
</Button>
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<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={2} lh={1.2}>
|
||||
List Daftar Informasi Publik
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
|
||||
>
|
||||
{isMobile ? 'Tambah' : 'Tambah Baru'}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" py="xl">
|
||||
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fw={500} c="dimmed">Belum ada informasi publik yang tersedia</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||
Belum ada informasi publik yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
striped
|
||||
stickyHeader
|
||||
style={{ minWidth: '700px' }}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Jenis Informasi</TableTh>
|
||||
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ textAlign: 'center' }}>
|
||||
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text fw={500} lineClamp={1}>{item.jenisInformasi}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text lineClamp={1} fz="sm" c="dimmed">
|
||||
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80) + '...'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
</TableTd>
|
||||
<TableTd style={{ textAlign: 'center' }}>
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
striped
|
||||
stickyHeader
|
||||
style={{ minWidth: '700px' }}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="25%">
|
||||
<Text fw={600} lh={1.4}>
|
||||
Jenis Informasi
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh w="40%">
|
||||
<Text fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh ta="center" w="20%">
|
||||
<Text fw={600} lh={1.4}>
|
||||
Aksi
|
||||
</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="sm" fw={600} lh={1.5} lineClamp={1}>
|
||||
{item.jenisInformasi}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
|
||||
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card List */}
|
||||
<Stack hiddenFrom="md" gap="sm">
|
||||
{filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text fw={600} lh={1.4}>
|
||||
Jenis Informasi
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.jenisInformasi}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="dimmed">
|
||||
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
<Center mt="lg">
|
||||
|
||||
<Center mt={{ base: 'lg', md: 'xl' }}>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
@@ -129,14 +202,12 @@ function ListDaftarInformasi({ search }: { search: string }) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DaftarInformasiPublik;
|
||||
export default DaftarInformasiPublik;
|
||||
@@ -8,11 +8,11 @@ import { useProxy } from 'valtio/utils';
|
||||
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const listDasarHukum = useProxy(stateDasarHukumPPID)
|
||||
const router = useRouter();
|
||||
const listDasarHukum = useProxy(stateDasarHukumPPID);
|
||||
useShallowEffect(() => {
|
||||
listDasarHukum.findById.load('1')
|
||||
}, [])
|
||||
listDasarHukum.findById.load('1');
|
||||
}, []);
|
||||
|
||||
if (listDasarHukum.findById.loading) {
|
||||
return (
|
||||
@@ -40,15 +40,16 @@ function Page() {
|
||||
<Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
w={{ base: '100%', md: "110%" }}
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
@@ -57,33 +58,39 @@ function Page() {
|
||||
<Grid>
|
||||
<GridCol span={12}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
|
||||
<Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={12}>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: '1.5rem', md: '2rem' }}
|
||||
fw="bold"
|
||||
<Title
|
||||
order={3}
|
||||
ta="center"
|
||||
lh={{ base: 1.15, md: 1.1 }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Divider my="xl" color={colors['blue-button']} />
|
||||
|
||||
<Box
|
||||
className="prose max-w-none"
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal", fontSize: '1.1rem', lineHeight: 1.7, textAlign: 'justify' }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.55,
|
||||
textAlign: 'justify',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -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, Tooltip } from '@mantine/core';
|
||||
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title} from '@mantine/core';
|
||||
import { IconChartBar, IconUsers } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -56,42 +56,77 @@ function LayoutTabsIKM({ 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", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((e, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={e.description}
|
||||
withArrow
|
||||
position="bottom"
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<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
|
||||
value={e.value}
|
||||
leftSection={e.icon}
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{e.label}
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
w="100%"
|
||||
>
|
||||
|
||||
<TabsList
|
||||
p="xs" // lebih kecil
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
width: "max-content", // ⬅️ kunci
|
||||
maxWidth: "100%", // ⬅️ penting
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value} mt="md">
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
|
||||
@@ -85,11 +85,11 @@ function EditResponden() {
|
||||
};
|
||||
|
||||
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} />
|
||||
</Button>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Responden
|
||||
</Title>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function DetailResponden() {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
|
||||
@@ -51,7 +51,7 @@ function RespondenCreate() {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack size={20} />
|
||||
@@ -96,24 +96,24 @@ function RespondenCreate() {
|
||||
}
|
||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||
/>
|
||||
<Select
|
||||
<Select
|
||||
key={"rating_responden"}
|
||||
label={"Rating"}
|
||||
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
||||
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
|
||||
onChange={(val) => {
|
||||
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||
/>
|
||||
label={"Rating"}
|
||||
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
||||
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
|
||||
onChange={(val) => {
|
||||
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||
/>
|
||||
<Select
|
||||
key={"kelompokUmur"}
|
||||
label={"Kelompok Umur"}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
'use client';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -9,11 +25,11 @@ import HeaderSearch from '../../../_com/header';
|
||||
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
|
||||
|
||||
function Responden() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Data Responden"
|
||||
title="Responden"
|
||||
placeholder="Cari nama responden..."
|
||||
searchIcon={<IconSearch size={18} />}
|
||||
value={search}
|
||||
@@ -33,17 +49,13 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
const router = useRouter();
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10)
|
||||
load(page, 10);
|
||||
}, [page]);
|
||||
|
||||
|
||||
const filteredData = (data || []).filter(item => {
|
||||
const filteredData = (data || []).filter((item) => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
return item.name.toLowerCase().includes(keyword);
|
||||
});
|
||||
|
||||
if (loading || !data) {
|
||||
@@ -56,21 +68,25 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
||||
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Title order={3}>Data Responden</Title>
|
||||
<Table striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ textAlign: 'center' }}>No</TableTh>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Jenis Kelamin</TableTh>
|
||||
<TableTh style={{ textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
<Text c="dimmed" ta="center" py="md">
|
||||
<Title order={2} lh={1.2}>
|
||||
Data Responden
|
||||
</Title>
|
||||
<Box visibleFrom="md">
|
||||
<Table striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh ta="center">No</TableTh>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Jenis Kelamin</TableTh>
|
||||
<TableTh ta="center">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
</Box>
|
||||
<Text c="dimmed" ta="center" py="md" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
|
||||
Belum ada data responden yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -79,54 +95,133 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
||||
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Title order={3}>Data Responden</Title>
|
||||
<Table striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh>
|
||||
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<Title order={2} lh={1.2}>
|
||||
Data Responden
|
||||
</Title>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Text c="dimmed" ta="center" py="md">
|
||||
Tidak ada data yang cocok dengan pencarian
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTh w="5%" ta="center">
|
||||
No
|
||||
</TableTh>
|
||||
<TableTh w="25%">Nama</TableTh>
|
||||
<TableTh w="25%">Tanggal</TableTh>
|
||||
<TableTh w="20%">Jenis Kelamin</TableTh>
|
||||
<TableTh w="15%" ta="center">
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
) : (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>{item.name}</TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>{item.jenisKelamin.name}</TableTd>
|
||||
<TableTd style={{ width: '15%', textAlign: 'center' }}>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
|
||||
Tidak ada data yang cocok dengan pencarian
|
||||
</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
) : (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd ta="center">{index + 1}</TableTd>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
|
||||
</TableTd>
|
||||
<TableTd>{item.jenisKelamin.name}</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Box hiddenFrom="md">
|
||||
{filteredData.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
|
||||
Tidak ada data yang cocok dengan pencarian
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{filteredData.map((item, index) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
No
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Tanggal
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.tanggal
|
||||
? new Date(item.tanggal).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jenis Kelamin
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.jenisKelamin.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box ta="center">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center>
|
||||
<Pagination
|
||||
@@ -138,7 +233,6 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
}}
|
||||
size="md"
|
||||
radius="md"
|
||||
mt="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
@@ -147,6 +241,4 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Responden;
|
||||
|
||||
|
||||
export default Responden;
|
||||
@@ -27,7 +27,7 @@ function DetailPermohonanInformasiPublik() {
|
||||
const data = state.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -39,7 +39,7 @@ function DetailPermohonanInformasiPublik() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -1,105 +1,277 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors'
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'
|
||||
import { useShallowEffect } from '@mantine/hooks'
|
||||
import { IconDeviceImacCog, IconId, IconInfoCircle, IconPhone, IconUser } from '@tabler/icons-react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import {
|
||||
IconDeviceImacCog,
|
||||
IconId,
|
||||
IconInfoCircle,
|
||||
IconPhone,
|
||||
IconSearch,
|
||||
IconUser,
|
||||
} from '@tabler/icons-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
|
||||
import { useDebouncedValue } from '@mantine/hooks'
|
||||
|
||||
function Page() {
|
||||
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
|
||||
const router = useRouter()
|
||||
const { data, page, totalPages, loading, load } = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
useShallowEffect(() => {
|
||||
permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.load()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
|
||||
<Skeleton radius="md" h={40} w="60%" />
|
||||
<Stack pos="relative" p="lg" align="center">
|
||||
<Skeleton radius="md" h={200} w="100%" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap={'sm'}>
|
||||
<Grid mb={10}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Title order={2} lh={1.2} c="dark">
|
||||
Daftar Permohonan Informasi Publik
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<Paper radius="lg" bg={colors['white-1']}>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder={"Cari nama..."}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
w="100%"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Paper>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Stack align="center" py="xl" ta="center">
|
||||
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||
{search
|
||||
? 'Tidak ditemukan data yang sesuai dengan pencarian'
|
||||
: 'Belum ada permohonan yang tercatat'
|
||||
}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const data = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data
|
||||
|
||||
return (
|
||||
<Box py="md">
|
||||
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Title order={2} c="dark">Daftar Permohonan Informasi Publik</Title>
|
||||
<IconInfoCircle size={20} stroke={1.5} />
|
||||
</Group>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap={'sm'}>
|
||||
<Grid mb={10}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Title order={2} lh={1.2} c="dark">
|
||||
Daftar Permohonan Informasi Publik
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<Paper radius="lg" bg={colors['white-1']}>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder={"Cari nama..."}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
w="100%"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Paper>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
{data.length === 0 ? (
|
||||
<Stack align="center" py="xl">
|
||||
<Stack align="center" py={{ base: 'xl', md: 'xl' }}>
|
||||
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fw={500} c="dimmed">Belum ada permohonan informasi yang tercatat</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||
Belum ada permohonan informasi yang tercatat
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
withRowBorders
|
||||
withColumnBorders
|
||||
withTableBorder
|
||||
striped
|
||||
stickyHeader
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh>
|
||||
<TableTh><Group gap={5}><IconId size={16} /> NIK</Group></TableTh>
|
||||
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh>
|
||||
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{data.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{index + 1}</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text lineClamp={1} fw={500}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
{item.nik}
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
{item.notelp}
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh fz="sm" fw={600} ta="center" w={60}>
|
||||
No
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600}>
|
||||
<Group gap={5}>
|
||||
<IconUser size={16} />
|
||||
Nama
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600}>
|
||||
<Group gap={5}>
|
||||
<IconId size={16} />
|
||||
NIK
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600}>
|
||||
<Group gap={5}>
|
||||
<IconPhone size={16} />
|
||||
Telepon
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} w={140}>
|
||||
<Group gap={5}>
|
||||
<IconInfoCircle size={16} />
|
||||
Detail
|
||||
</Group>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{data.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd ta="center" fz="sm" lh={1.5}>
|
||||
{index + 1}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5}>{item.nik}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5}>{item.notelp}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{data.map((item, index) => (
|
||||
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||
No
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="dark">
|
||||
{index + 1}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c="dark" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||
NIK
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5} c="dark">{item.nik}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||
Telepon
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5} c="dark">{item.notelp}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||
Detail
|
||||
</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
|
||||
}
|
||||
mt={2}
|
||||
>
|
||||
Detail
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
withControls
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
@@ -28,7 +28,7 @@ function DetailPermohonanKeberatanInformasiPublik() {
|
||||
const data = state.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box py="md">
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
@@ -40,7 +40,7 @@ function DetailPermohonanKeberatanInformasiPublik() {
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -1,97 +1,285 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors'
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'
|
||||
import { useShallowEffect } from '@mantine/hooks'
|
||||
import { IconDeviceImacCog, IconInfoCircle, IconMail, IconPhone, IconUser } from '@tabler/icons-react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import {
|
||||
IconDeviceImacCog,
|
||||
IconInfoCircle,
|
||||
IconMail,
|
||||
IconPhone,
|
||||
IconSearch,
|
||||
IconUser,
|
||||
} from '@tabler/icons-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDebouncedValue } from '@mantine/hooks'
|
||||
|
||||
function Page() {
|
||||
const listState = useProxy(statePermohonanKeberatan)
|
||||
const router = useRouter()
|
||||
useShallowEffect(() => {
|
||||
listState.findMany.load()
|
||||
}, [])
|
||||
const listState = useProxy(statePermohonanKeberatan)
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const { data, page, totalPages, loading, load } = listState.findMany
|
||||
|
||||
if (!listState.findMany.data) {
|
||||
useEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
|
||||
<Skeleton radius="md" h={40} w="60%" />
|
||||
<Stack pos="relative" p="lg" align="center">
|
||||
<Skeleton radius="md" h={200} w="100%" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const data = listState.findMany.data
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap={'sm'}>
|
||||
<Grid mb={10}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Title order={2} lh={1.2} c="dark">
|
||||
Daftar Permohonan Keberatan Informasi Publik
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<Paper radius="lg" bg={colors['white-1']}>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder={"Cari nama..."}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
w="100%"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Paper>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Stack align="center" py="xl" ta="center">
|
||||
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||
{search
|
||||
? 'Tidak ditemukan data yang sesuai dengan pencarian'
|
||||
: 'Belum ada permohonan keberatan yang tercatat'
|
||||
}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="md">
|
||||
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Title order={2} c="dark">Daftar Permohonan Keberatan Informasi Publik</Title>
|
||||
<IconInfoCircle size={20} stroke={1.5} />
|
||||
</Group>
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap={'sm'}>
|
||||
<Grid mb={10}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Title order={2} lh={1.2} c="dark">
|
||||
Daftar Permohonan Keberatan Informasi Publik
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<Paper radius="lg" bg={colors['white-1']}>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder={"Cari nama..."}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
w="100%"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Paper>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
{data.length === 0 ? (
|
||||
<Stack align="center" py="xl">
|
||||
<Stack align="center" py="xl" ta="center">
|
||||
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fw={500} c="dimmed">Belum ada permohonan keberatan yang tercatat</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||
Belum ada permohonan keberatan yang tercatat
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
withRowBorders
|
||||
withColumnBorders
|
||||
withTableBorder
|
||||
striped
|
||||
stickyHeader
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh>
|
||||
<TableTh><Group gap={5}><IconMail size={16} /> Email</Group></TableTh>
|
||||
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh>
|
||||
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{data.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{index + 1}</TableTd>
|
||||
<TableTd>
|
||||
<Text lineClamp={1} fw={500}>{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text size="sm">{item.email || '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text>{item.notelp || '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh fz="sm" fw={600} lh={1.4} ta="center">
|
||||
No
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
<Group gap={5}>
|
||||
<IconUser size={16} />
|
||||
Nama
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
<Group gap={5}>
|
||||
<IconMail size={16} />
|
||||
Email
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
<Group gap={5}>
|
||||
<IconPhone size={16} />
|
||||
Telepon
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4} ta="center">
|
||||
<Group gap={5}>
|
||||
<IconInfoCircle size={16} />
|
||||
Detail
|
||||
</Group>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{data.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd ta="center" fz="sm" lh={1.5}>
|
||||
{index + 1}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
{item.email || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
{item.notelp || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="xs">
|
||||
{data.map((item, index) => (
|
||||
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
|
||||
No
|
||||
</Text>
|
||||
<Text fz="sm" fw={600} lh={1.5}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={600} lh={1.5}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
|
||||
Email
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
{item.email || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
|
||||
Telepon
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
{item.notelp || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`
|
||||
)
|
||||
}
|
||||
mt={4}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
withControls
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
export default Page;
|
||||
|
||||
export default Page
|
||||
@@ -138,7 +138,7 @@ function EditProfilePPID() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p="md">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Stack gap="md">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
|
||||
@@ -7,13 +7,13 @@ import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
|
||||
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const allList = useProxy(stateProfilePPID)
|
||||
const router = useRouter();
|
||||
const allList = useProxy(stateProfilePPID);
|
||||
|
||||
useShallowEffect(() => {
|
||||
allList.profile.load("edit") // Assuming "1" is your default ID, adjust as needed
|
||||
}, [])
|
||||
allList.profile.load("edit");
|
||||
}, []);
|
||||
|
||||
if (!allList.profile.data) {
|
||||
return (
|
||||
@@ -32,19 +32,19 @@ function Page() {
|
||||
<Stack gap="md">
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
||||
<Title order={3} c={colors['blue-button']} lh={1.2}>Preview Profil PPID</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
w={{base: '100%', md: "110%"}}
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
w={{ base: '100%', md: "110%" }}
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
{dataArray.map((item) => (
|
||||
@@ -57,9 +57,14 @@ function Page() {
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={12}>
|
||||
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={2}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
lh={1.15}
|
||||
>
|
||||
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
|
||||
</Text>
|
||||
</Title>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
@@ -87,34 +92,77 @@ function Page() {
|
||||
className="glass3"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
<Title
|
||||
order={3}
|
||||
c={colors['white-1']}
|
||||
ta="center"
|
||||
lh={1.2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Title>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
|
||||
<Box mt="lg">
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.biodata }} />
|
||||
<Title order={3} lh={1.2} mb={4}>
|
||||
Biodata
|
||||
</Title>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.biodata }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box mt="xl">
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Riwayat Karir</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.riwayat }} />
|
||||
</Box>
|
||||
|
||||
<Box mt="xl">
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Pengalaman Organisasi</Text>
|
||||
<Title order={3} lh={1.2} mb={4}>
|
||||
Riwayat Karir
|
||||
</Title>
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.pengalaman }} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.riwayat }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="xl" mb="lg">
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Program Kerja Unggulan</Text>
|
||||
|
||||
<Box mt="xl">
|
||||
<Title order={3} lh={1.2} mb={4}>
|
||||
Pengalaman Organisasi
|
||||
</Title>
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.unggulan }} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.pengalaman }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="xl" mb="lg">
|
||||
<Title order={3} lh={1.2} mb={4}>
|
||||
Program Kerja Unggulan
|
||||
</Title>
|
||||
<Box px={20}>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.unggulan }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -122,9 +170,7 @@ function Page() {
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Page;
|
||||
@@ -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 { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -63,51 +63,92 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
keepMounted={false}
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
w="100%"
|
||||
>
|
||||
|
||||
<TabsList
|
||||
p="xs" // lebih kecil
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
width: "max-content", // ⬅️ kunci
|
||||
maxWidth: "100%", // ⬅️ penting
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function EditPegawaiPPID() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -51,7 +51,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} />
|
||||
@@ -59,7 +59,7 @@ function DetailPegawai() {
|
||||
</Box>
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -78,7 +78,7 @@ function CreatePegawaiPPID() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
/* 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 {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } 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 stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
|
||||
function PegawaiPPID() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -28,6 +47,7 @@ function PegawaiPPID() {
|
||||
function ListPegawaiPPID({ search }: { search: string }) {
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -38,47 +58,28 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
} = stateOrganisasi.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
// Handle loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={300} />
|
||||
<Stack py="xl">
|
||||
<Skeleton height={300} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Box py="xl">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Pegawai PPID</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/ppid/struktur-ppid/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 PPID</Title>
|
||||
<Title order={2} lh={1.2}>
|
||||
Daftar Pegawai PPID
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
@@ -87,53 +88,70 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Tidak ada data pegawai yang ditemukan
|
||||
</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="xl">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={2} lh={1.2}>
|
||||
Daftar Pegawai PPID
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
|
||||
{/* Desktop: Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Status</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
Nama Lengkap
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
Posisi
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
Status
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={150}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.namaLengkap}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end">
|
||||
{item.namaLengkap}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={150}>
|
||||
<Badge variant="light" color="blue">
|
||||
{item.posisi?.nama || 'Belum diatur'}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Badge variant="light" color="blue" fz="sm" lh={1.4}>
|
||||
{item.posisi?.nama || 'Belum diatur'}
|
||||
</Badge>
|
||||
</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>
|
||||
<Badge color={item.isActive ? "green" : "red"} fz="sm" lh={1.4}>
|
||||
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
@@ -143,6 +161,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
|
||||
fz="sm"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
@@ -152,7 +171,47 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
<Center mt="lg">
|
||||
|
||||
{/* Mobile: Card List */}
|
||||
<Stack hiddenFrom="md" gap="sm" mt="md">
|
||||
{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 Lengkap</Text>
|
||||
<Text fz="md" fw={500} lh={1.4}>
|
||||
{item.namaLengkap}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Posisi</Text>
|
||||
<Badge variant="light" color="blue" fz="xs" lh={1.4}>
|
||||
{item.posisi?.nama || 'Belum diatur'}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
|
||||
<Badge color={item.isActive ? "green" : "red"} fz="xs" lh={1.4}>
|
||||
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
|
||||
fz="xs"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
@@ -170,4 +229,4 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default PegawaiPPID;
|
||||
export default PegawaiPPID;
|
||||
@@ -107,7 +107,7 @@ function EditPosisiOrganisasiPPID() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -46,7 +46,7 @@ function CreatePosisiOrganisasiPPID() {
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -31,6 +31,7 @@ function ListPosisiOrganisasiPPID({ 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,
|
||||
@@ -41,8 +42,8 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
} = stateOrganisasi.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const handleHapus = async () => {
|
||||
if (selectedId) {
|
||||
@@ -56,63 +57,63 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
|
||||
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 Posisi Organisasi PPID</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={2}>Daftar Posisi Organisasi PPID</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<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>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Nama Posisi</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Deskripsi</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Hierarki</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>Edit</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>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>
|
||||
<Text fz="md" fw={600} lh={1.5} 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 w={200}>
|
||||
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Text>{item.hierarki || '-'}</Text>
|
||||
<TableTd>
|
||||
<Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
@@ -129,9 +130,11 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={{ base: 'sm', md: 'md' }}>
|
||||
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
|
||||
Tidak ada data posisi organisasi yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -139,7 +142,59 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<Stack gap="xs" hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Nama Posisi</Text>
|
||||
<Text fz="sm" fw={600} lh={1.5}>{item.nama}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" fw={600} lh={1.4}>Hierarki</Text>
|
||||
<Text fz="sm" lh={1.5}>{item.hierarki || '-'}</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="xs"
|
||||
onClick={() => router.push(`/admin/ppid/struktur-ppid/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 fz="sm" c="dimmed" ta="center" lh={1.5}>
|
||||
Tidak ada data posisi organisasi yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -154,6 +209,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
@@ -165,4 +221,4 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default PosisiOrganisasiPPID;
|
||||
export default PosisiOrganisasiPPID;
|
||||
@@ -71,7 +71,7 @@ function VisiMisiPPIDEdit() {
|
||||
};
|
||||
|
||||
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,9 +7,8 @@ import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
|
||||
|
||||
|
||||
function VisiMisiPPIDList() {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
const listVisiMisi = useProxy(stateVisiMisiPPID);
|
||||
useShallowEffect(() => {
|
||||
listVisiMisi.findById.load('1');
|
||||
@@ -41,15 +40,16 @@ function VisiMisiPPIDList() {
|
||||
<Title order={3} c={colors['blue-button']}>Preview Visi Misi PPID</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
w={{ base: '100%', md: "110%" }}
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
@@ -58,14 +58,25 @@ function VisiMisiPPIDList() {
|
||||
<Grid>
|
||||
<GridCol span={12}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
|
||||
<Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={12}>
|
||||
<Text ta="center" fz={{ base: '1.2rem', md: '1.8rem' }} fw="bold" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={2}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: 1.15 }}
|
||||
>
|
||||
MOTO PPID DESA DARMASABA
|
||||
</Text>
|
||||
<Text ta="center" fz={{ base: '1rem', md: '1.2rem' }} mt="sm">
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.5 }}
|
||||
mt="sm"
|
||||
c="black"
|
||||
>
|
||||
MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
|
||||
</Text>
|
||||
</GridCol>
|
||||
@@ -74,26 +85,50 @@ function VisiMisiPPIDList() {
|
||||
<Divider my="xl" color={colors['blue-button']} />
|
||||
|
||||
<Box>
|
||||
<Text fz={{ base: '1.5rem', md: '1.75rem' }} fw="bold" ta="center" mb="lg" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={2}
|
||||
ta="center"
|
||||
mb="lg"
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: 1.15 }}
|
||||
>
|
||||
VISI PPID
|
||||
</Text>
|
||||
<Box
|
||||
className="prose max-w-none"
|
||||
</Title>
|
||||
<Text
|
||||
ta={{ base: "center", md: "justify" }}
|
||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal", textAlign: "justify"}}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.55,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" color={colors['blue-button']} />
|
||||
|
||||
<Box mt="xl">
|
||||
<Text fz={{ base: '1.5rem', md: '1.75rem' }} fw="bold" ta="center" mb="lg" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={2}
|
||||
ta="center"
|
||||
mb="lg"
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: 1.15 }}
|
||||
>
|
||||
MISI PPID
|
||||
</Text>
|
||||
<Box
|
||||
className="prose max-w-none"
|
||||
</Title>
|
||||
<Text
|
||||
ta={"justify"}
|
||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal", textAlign: "justify"}}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.55,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -103,4 +138,4 @@ function VisiMisiPPIDList() {
|
||||
);
|
||||
}
|
||||
|
||||
export default VisiMisiPPIDList;
|
||||
export default VisiMisiPPIDList;
|
||||
@@ -33,7 +33,7 @@ import { useEffect, useState } from "react";
|
||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close'
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
@@ -45,21 +45,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
credentials: 'include' // ✅ ADD credentials
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.user) {
|
||||
// ✅ Check if user is NOT active → redirect to waiting room
|
||||
if (!data.user.isActive) {
|
||||
authStore.setUser(null);
|
||||
router.replace('/waiting-room');
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Fetch menuIds
|
||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
||||
credentials: 'include' // ✅ ADD credentials
|
||||
credentials: 'include'
|
||||
});
|
||||
const menuData = await menuRes.json();
|
||||
|
||||
@@ -67,7 +65,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
? [...menuData.menuIds]
|
||||
: null;
|
||||
|
||||
// ✅ Set user dengan menuIds yang fresh
|
||||
authStore.setUser({
|
||||
id: data.user.id,
|
||||
name: data.user.name,
|
||||
@@ -76,7 +73,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
isActive: data.user.isActive
|
||||
});
|
||||
|
||||
// ✅ IMPROVED: Redirect ONLY if di root /admin
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (currentPath === '/admin') {
|
||||
@@ -84,7 +80,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
||||
router.replace(expectedPath);
|
||||
}
|
||||
// ✅ Jangan redirect jika user sudah di path yang valid
|
||||
|
||||
} else {
|
||||
authStore.setUser(null);
|
||||
@@ -100,17 +95,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [router]); // ✅ Only depend on router
|
||||
}, [router]);
|
||||
|
||||
const getRedirectPath = (roleId: number): string => {
|
||||
switch (roleId) {
|
||||
case 0: // DEVELOPER
|
||||
case 1: // SUPERADMIN
|
||||
case 2: // ADMIN_DESA
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
return '/admin/landing-page/profil/program-inovasi';
|
||||
case 3: // ADMIN_KESEHATAN
|
||||
case 3:
|
||||
return '/admin/kesehatan/posyandu';
|
||||
case 4: // ADMIN_PENDIDIKAN
|
||||
case 4:
|
||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
default:
|
||||
return '/admin';
|
||||
@@ -139,7 +134,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include' // ✅ ADD credentials
|
||||
credentials: 'include'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
@@ -163,6 +158,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Handler untuk menutup mobile menu saat navigasi
|
||||
const handleNavClick = (path: string) => {
|
||||
router.push(path);
|
||||
close(); // Tutup mobile menu
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
suppressHydrationWarning
|
||||
@@ -177,7 +178,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
|
||||
<AppShellHeader
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||
@@ -230,16 +230,48 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</AppShellHeader>
|
||||
|
||||
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
||||
{/* ... Navbar content sama seperti sebelumnya ... */}
|
||||
<AppShell.Section p="sm">
|
||||
{currentNav.map((v, k) => {
|
||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||
return (
|
||||
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
|
||||
<NavLink
|
||||
key={k}
|
||||
defaultOpened={isParentActive}
|
||||
c={isParentActive ? colors["blue-button"] : "gray"}
|
||||
label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>}
|
||||
style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }}
|
||||
styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }}
|
||||
variant="light"
|
||||
active={isParentActive}
|
||||
>
|
||||
{v.children.map((child, key) => {
|
||||
const isChildActive = segments.includes(_.lowerCase(child.name));
|
||||
return (
|
||||
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
|
||||
<NavLink
|
||||
key={key}
|
||||
// ✅ PERBAIKAN: Gunakan onClick untuk handle navigasi dan close menu
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleNavClick(child.path);
|
||||
}}
|
||||
href={child.path}
|
||||
c={isChildActive ? colors["blue-button"] : "gray"}
|
||||
label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: rem(8),
|
||||
marginBottom: rem(2),
|
||||
transition: 'background 150ms ease',
|
||||
padding: '6px 12px',
|
||||
'&:hover': {
|
||||
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)'
|
||||
},
|
||||
...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' })
|
||||
}
|
||||
}}
|
||||
active={isChildActive}
|
||||
component={Link}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</NavLink>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function grafikJumlahPendudukMiskinFindMany(
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
orderBy: { year: "asc" },
|
||||
}),
|
||||
prisma.grafikJumlahPendudukMiskin.count({
|
||||
where,
|
||||
|
||||
@@ -1,14 +1,56 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function permohonanInformasiPublikFindMany() {
|
||||
const res = await prisma.permohonanInformasiPublik.findMany({
|
||||
include: {
|
||||
jenisInformasiDiminta: true,
|
||||
caraMemperolehInformasi: true,
|
||||
caraMemperolehSalinanInformasi: true,
|
||||
}
|
||||
});
|
||||
return {
|
||||
data: res,
|
||||
};
|
||||
export default async function permohonanInformasiPublikFindMany(
|
||||
context: Context
|
||||
) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: "insensitive" } },
|
||||
{ email: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.permohonanInformasiPublik.findMany({
|
||||
where,
|
||||
skip,
|
||||
include: {
|
||||
jenisInformasiDiminta: true,
|
||||
caraMemperolehInformasi: true,
|
||||
caraMemperolehSalinanInformasi: true,
|
||||
},
|
||||
take: limit,
|
||||
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.permohonanInformasiPublik.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch formulir permohonan keberatan with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch formulir permohonan keberatan with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function permohonanKeberatanInformasiPublikFindMany() {
|
||||
const res = await prisma.formulirPermohonanKeberatan.findMany();
|
||||
return {
|
||||
data: res,
|
||||
};
|
||||
export default async function permohonanKeberatanInformasiPublikFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{email: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.formulirPermohonanKeberatan.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.formulirPermohonanKeberatan.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch formulir permohonan keberatan with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch formulir permohonan keberatan with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton } from '@mantine/core';
|
||||
import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton, Title } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { BarChart } from '@mantine/charts';
|
||||
@@ -32,23 +32,47 @@ function Page() {
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
Demografi Pekerjaan
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
style={{ lineHeight: 1.5 }}
|
||||
>
|
||||
Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan
|
||||
</Text>
|
||||
<Text ta={'center'} fz={'h4'}>Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Paper p={'xl'}>
|
||||
<Box style={{overflowX: 'scroll'}}>
|
||||
<Text pb={5} fw={'bold'} fz={'h4'}>Statistik Demografi Pekerjaan Di Desa Darmasaba</Text>
|
||||
<Box style={{ overflowX: 'auto' }} w={"100%"}>
|
||||
<Text
|
||||
pb={5}
|
||||
fw={'bold'}
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.2}
|
||||
c="black"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
Statistik Demografi Pekerjaan Di Desa Darmasaba
|
||||
</Text>
|
||||
<BarChart
|
||||
type='stacked'
|
||||
p={10}
|
||||
mb={50}
|
||||
h={400}
|
||||
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
|
||||
w={Math.max(data.length * 120, 800)}
|
||||
data={data.map((item) => ({
|
||||
id: item.id,
|
||||
Pekerjaan: item.pekerjaan,
|
||||
@@ -62,28 +86,45 @@ function Page() {
|
||||
]}
|
||||
tickLine="y"
|
||||
xAxisProps={{
|
||||
angle: -45, // Rotate labels by -45 degrees
|
||||
textAnchor: 'end', // Anchor text to the end for better alignment
|
||||
height: 100, // Increase height for rotated labels
|
||||
interval: 0, // Show all labels
|
||||
angle: -45,
|
||||
textAnchor: 'end',
|
||||
height: 100,
|
||||
interval: 0,
|
||||
style: {
|
||||
fontSize: '12px', // Adjust font size if needed
|
||||
fontSize: '12px',
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'nowrap'
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.4,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{base: 7, md: 5}} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Laki-Laki</Text>
|
||||
<Flex gap={{ base: 7, md: 5 }} align={'center'}>
|
||||
<Text
|
||||
fw={'bold'}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.2}
|
||||
c="black"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
Laki-Laki
|
||||
</Text>
|
||||
<ColorSwatch color="#5082EE" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{base: 7, md: 5}} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Perempuan</Text>
|
||||
<Flex gap={{ base: 7, md: 5 }} align={'center'}>
|
||||
<Text
|
||||
fw={'bold'}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.2}
|
||||
c="black"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
Perempuan
|
||||
</Text>
|
||||
<ColorSwatch color="#6EDF9C" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -95,4 +136,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -2,7 +2,7 @@
|
||||
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
|
||||
import colors from '@/con/colors';
|
||||
import { BarChart } from '@mantine/charts';
|
||||
import { Box, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Paper, Skeleton, Stack, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -17,13 +17,10 @@ function Page() {
|
||||
const state = useProxy(jumlahPendudukMiskin)
|
||||
const [chartData, setChartData] = useState<JPMGrafik[]>([])
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load()
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (state.findMany.data) {
|
||||
setChartData(state.findMany.data.map((item) => ({
|
||||
@@ -48,20 +45,30 @@ function Page() {
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Title
|
||||
order={1}
|
||||
ta={"center"}
|
||||
c={colors["blue-button"]}
|
||||
fw={"bold"}
|
||||
lh={1.1}
|
||||
>
|
||||
Jumlah Penduduk Miskin
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Paper p={'xl'}>
|
||||
<Text fz={'h3'}>Jumlah Data Penduduk Miskin</Text>
|
||||
<Text fw={"bold"} fz={'h1'}>
|
||||
<Title order={3} fw={'normal'} lh={1.1}>
|
||||
Jumlah Data Penduduk Miskin
|
||||
</Title>
|
||||
<Title order={2} fw={"bold"} lh={1.1}>
|
||||
{state.findMany.data?.reduce((sum, item) => sum + (Number(item.totalPoorPopulation) || 0), 0).toLocaleString()} Orang
|
||||
</Text>
|
||||
</Title>
|
||||
</Paper>
|
||||
<Paper p={'xl'}>
|
||||
<Text pb={10} fw={'bold'} fz={'h4'}>Jumlah Penduduk Miskin Per Tahun</Text>
|
||||
<Title order={3} pb={10} fw={'bold'} lh={1.1}>
|
||||
Jumlah Penduduk Miskin Per Tahun
|
||||
</Title>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
@@ -79,4 +86,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -3,7 +3,7 @@
|
||||
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
|
||||
import colors from '@/con/colors';
|
||||
import { PieChart } from '@mantine/charts';
|
||||
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -56,7 +56,7 @@ function Page() {
|
||||
}
|
||||
}, [stateGrafikNganggurPendidikan.findMany.data])
|
||||
|
||||
if (!stateGrafikNganggur.findMany.data) {
|
||||
if (!stateGrafikNganggur.findMany.data || !stateGrafikNganggurPendidikan.findMany.data) {
|
||||
return (
|
||||
<Box>
|
||||
<Skeleton h={500} />
|
||||
@@ -64,114 +64,151 @@ function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!stateGrafikNganggur.findMany.data) {
|
||||
return (
|
||||
<Box>
|
||||
<Skeleton h={500} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
style={{ lineHeight: 1.15 }}
|
||||
>
|
||||
Jumlah Penduduk Usia Kerja Yang Menganggur
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 50, lg: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Paper p={'lg'}>
|
||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
|
||||
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
||||
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
|
||||
<PieChart
|
||||
w="100%"
|
||||
h={250} // lebih kecil biar aman di mobile
|
||||
withLabelsLine
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabels
|
||||
data={donutGrafikNganggurData}
|
||||
withTooltip
|
||||
tooltipDataSource="segment"
|
||||
/>
|
||||
<Title
|
||||
order={2}
|
||||
fw="bold"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
Pengangguran Berdasarkan Usia
|
||||
</Title>
|
||||
{mounted && donutGrafikNganggurData.length > 0 ? (
|
||||
<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
||||
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
|
||||
<PieChart
|
||||
w="100%"
|
||||
h={250}
|
||||
withLabelsLine
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabels
|
||||
data={donutGrafikNganggurData}
|
||||
withTooltip
|
||||
tooltipDataSource="segment"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>) : <Skeleton h={500} />}
|
||||
) : (
|
||||
<Skeleton h={500} />
|
||||
)}
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>18-25</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
18-25
|
||||
</Text>
|
||||
<ColorSwatch color="#4b6Ef5" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>26-35</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
26-35
|
||||
</Text>
|
||||
<ColorSwatch color="#14b885" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>36-45</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
36-45
|
||||
</Text>
|
||||
<ColorSwatch color="#E6A03B" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>46+</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
46+
|
||||
</Text>
|
||||
<ColorSwatch color="#DB524D" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Paper>
|
||||
<Paper p={'lg'}>
|
||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Pendidikan</Text>
|
||||
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (<Center>
|
||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||
<PieChart
|
||||
w="100%"
|
||||
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
|
||||
withLabelsLine
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabels
|
||||
data={donutGrafikNganggurDataPendidikan}
|
||||
withTooltip
|
||||
tooltipDataSource="segment"
|
||||
/>
|
||||
</Box>
|
||||
</Center>) : <Skeleton h={500} />}
|
||||
<Title
|
||||
order={2}
|
||||
fw="bold"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
Pengangguran Berdasarkan Pendidikan
|
||||
</Title>
|
||||
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (
|
||||
<Center>
|
||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||
<PieChart
|
||||
w="100%"
|
||||
h="min(250px, 50vh)"
|
||||
withLabelsLine
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabels
|
||||
data={donutGrafikNganggurDataPendidikan}
|
||||
withTooltip
|
||||
tooltipDataSource="segment"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
) : (
|
||||
<Skeleton h={500} />
|
||||
)}
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SD</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
SD
|
||||
</Text>
|
||||
<ColorSwatch color="#4b6Ef5" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMP</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
SMP
|
||||
</Text>
|
||||
<ColorSwatch color="#14b885" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMA/SMK</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
SMA/SMK
|
||||
</Text>
|
||||
<ColorSwatch color="#E6A03B" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>D3</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
D3
|
||||
</Text>
|
||||
<ColorSwatch color="#DB524D" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>S1</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||
S1
|
||||
</Text>
|
||||
<ColorSwatch color="#1018A8FF" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -183,4 +220,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -36,7 +36,6 @@ function Page() {
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (state.findMany.data) {
|
||||
// Set chart data
|
||||
setChartData(state.findMany.data.map((item) => ({
|
||||
id: item.id,
|
||||
bulan: item.month,
|
||||
@@ -44,7 +43,6 @@ function Page() {
|
||||
takberpendidikan: Number(item.uneducatedUnemployment),
|
||||
})));
|
||||
|
||||
// Calculate yearly totals
|
||||
const currentYearData = state.findMany.data.filter(item => item.year === currentYear);
|
||||
if (currentYearData.length > 0) {
|
||||
const yearlyTotal = {
|
||||
@@ -72,30 +70,37 @@ function Page() {
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta={"center"}
|
||||
c={colors["blue-button"]}
|
||||
fw={"bold"}
|
||||
lh={1.2}
|
||||
>
|
||||
Jumlah Pengangguran
|
||||
</Text>
|
||||
</Title>
|
||||
<Group py={20} align='center' justify='space-between'>
|
||||
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
|
||||
<Title order={2} fw={"bold"} lh={1.2}>
|
||||
DATA PENGANGGURAN DESA
|
||||
</Title>
|
||||
</Group>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<SimpleGrid
|
||||
cols={1}
|
||||
pb={20}
|
||||
>
|
||||
<SimpleGrid cols={1} pb={20}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||
{/* Total Unemployment Card */}
|
||||
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
||||
<Flex direction="column" gap="md">
|
||||
<IconUserOff size={35} color={colors['blue-button']} />
|
||||
<Text fz="h4" fw={600}>Total Pengangguran</Text>
|
||||
<Text fz="h2" fw={700} c={colors['blue-button']}>
|
||||
<Title order={3} fw={600} lh={1.2}>
|
||||
Total Pengangguran
|
||||
</Title>
|
||||
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c={colors['blue-button']} lh={1.2}>
|
||||
{yearlyData?.total.toLocaleString() || 0} Orang
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
|
||||
Total data tahun {currentYear}
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -105,11 +110,13 @@ function Page() {
|
||||
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
||||
<Flex direction="column" gap="md">
|
||||
<IconSchool size={35} color="#5082EE" />
|
||||
<Text fz="h4" fw={600}>Pengangguran Terdidik</Text>
|
||||
<Text fz="h2" fw={700} c="#5082EE">
|
||||
<Title order={3} fw={600} lh={1.2}>
|
||||
Pengangguran Terdidik
|
||||
</Title>
|
||||
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c="#5082EE" lh={1.2}>
|
||||
{yearlyData?.educated.toLocaleString() || 0} Orang
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
|
||||
{yearlyData ?
|
||||
<>
|
||||
{((yearlyData.educated / yearlyData.total) * 100).toFixed(1)}%
|
||||
@@ -123,11 +130,13 @@ function Page() {
|
||||
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
||||
<Flex direction="column" gap="md">
|
||||
<IconSchoolOff size={35} color="#DA524C" />
|
||||
<Text fz="h4" fw={600}>Pengangguran Tidak Terdidik</Text>
|
||||
<Text fz="h2" fw={700} c="#DA524C">
|
||||
<Title order={3} fw={600} lh={1.2}>
|
||||
Pengangguran Tidak Terdidik
|
||||
</Title>
|
||||
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c="#DA524C" lh={1.2}>
|
||||
{yearlyData?.uneducated.toLocaleString() || 0} Orang
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
|
||||
{yearlyData ?
|
||||
<>
|
||||
{((yearlyData.uneducated / yearlyData.total) * 100).toFixed(1)}%
|
||||
@@ -142,13 +151,17 @@ function Page() {
|
||||
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Berpendidikan</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Pengangguran Berpendidikan
|
||||
</Text>
|
||||
<ColorSwatch color="#5082EE" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Tak Berpendidikan</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Pengangguran Tak Berpendidikan
|
||||
</Text>
|
||||
<ColorSwatch color="#DA524C" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -156,15 +169,24 @@ function Page() {
|
||||
{!mounted || chartData.length === 0 ? (
|
||||
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
|
||||
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
|
||||
<Title order={3} pb={10} lh={1.2}>
|
||||
Data Pengangguran Terdidik dan Tidak Terdidik
|
||||
</Title>
|
||||
<Text c='dimmed' fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
|
||||
<Box w={{ base: '100%', md: '70%' }}>
|
||||
<Title order={3} pb={10} lh={1.2}>
|
||||
Data Pengangguran Terdidik dan Tidak Terdidik
|
||||
</Title>
|
||||
<Box
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
style={{ overflowX: "auto" }}
|
||||
>
|
||||
<BarChart
|
||||
h={450}
|
||||
data={chartData}
|
||||
@@ -178,32 +200,55 @@ function Page() {
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Paper>
|
||||
<Paper p={'lg'}>
|
||||
<Text fw={'bold'} fz={'h4'}>Detail Data Pengangguran</Text>
|
||||
<Table striped highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh ta={'center'}>Bulan</TableTh>
|
||||
<TableTh ta={'center'}>Total</TableTh>
|
||||
<TableTh ta={'center'}>Terdidik</TableTh>
|
||||
<TableTh ta={'center'}>Tidak Terdidik</TableTh>
|
||||
<TableTh ta={'center'}>Perubahan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{state.findMany.data?.map((item, index) => (
|
||||
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
|
||||
<TableTd ta={'center'}>{item.month}</TableTd>
|
||||
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
|
||||
<Title order={2} fw={'bold'} fz={{ base: 'md', md: 'lg' }} lh={1.2}>
|
||||
Detail Data Pengangguran
|
||||
</Title>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Bulan
|
||||
</TableTh>
|
||||
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Total
|
||||
</TableTh>
|
||||
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Terdidik
|
||||
</TableTh>
|
||||
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Tidak Terdidik
|
||||
</TableTh>
|
||||
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
Perubahan
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{state.findMany.data?.map((item, index) => (
|
||||
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
|
||||
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
{item.month}
|
||||
</TableTd>
|
||||
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
{item.totalUnemployment}
|
||||
</TableTd>
|
||||
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
{item.educatedUnemployment}
|
||||
</TableTd>
|
||||
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
{item.uneducatedUnemployment}
|
||||
</TableTd>
|
||||
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||
{item.percentageChange}%
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -211,4 +256,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -33,18 +33,25 @@ function DetailLowonganKerjaUser() {
|
||||
);
|
||||
}
|
||||
|
||||
const formatRupiah = (value: number) =>
|
||||
new Intl.NumberFormat("id-ID", {
|
||||
style: "currency",
|
||||
currency: "IDR",
|
||||
minimumFractionDigits: 0,
|
||||
}).format(value);
|
||||
|
||||
return (
|
||||
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
|
||||
<Box w={{ base: '100%', md: '70%' }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
leftSection={<IconArrowBack size={20} />}
|
||||
mb="md"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
leftSection={<IconArrowBack size={20} />}
|
||||
mb="md"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
radius="lg"
|
||||
@@ -54,11 +61,17 @@ function DetailLowonganKerjaUser() {
|
||||
bg={colors['white-1']}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
{/* Judul */}
|
||||
<Text fz={{ base: '1.6rem', md: '2rem' }} fw={700} c={colors['blue-button']}>
|
||||
{/* Judul Posisi - H1 */}
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: 1.15 }}
|
||||
>
|
||||
{data.posisi}
|
||||
</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
</Title>
|
||||
|
||||
{/* Tanggal Posting - Caption */}
|
||||
<Text c="dimmed" fz={{ base: 12, md: 'sm' }} lh={1.4}>
|
||||
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
@@ -70,44 +83,72 @@ function DetailLowonganKerjaUser() {
|
||||
<Stack gap="sm" mt="md">
|
||||
<Group gap="xs">
|
||||
<IconBriefcase size={20} color={colors['blue-button']} />
|
||||
<Text fz="md" fw={600}>{data.namaPerusahaan}</Text>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
fw={600}
|
||||
lh={1.5}
|
||||
>
|
||||
{data.namaPerusahaan}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={20} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.lokasi}</Text>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
>
|
||||
{data.lokasi}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconPhone size={20} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.notelp}</Text>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
>
|
||||
{data.notelp}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconCurrencyDollar size={20} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.gaji || '-'}</Text>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
>
|
||||
{formatRupiah(Number(data.gaji)) || '-'}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconBriefcase size={20} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.tipePekerjaan}</Text>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
>
|
||||
{data.tipePekerjaan}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{/* Deskripsi Pekerjaan - H2 */}
|
||||
<Box>
|
||||
<Text fw={600} fz="lg" mb={4}>
|
||||
<Title order={2} mb={8} style={{ lineHeight: 1.2 }}>
|
||||
Deskripsi Pekerjaan
|
||||
</Text>
|
||||
</Title>
|
||||
<Text
|
||||
fz="sm"
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={1.6}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Kualifikasi - H2 */}
|
||||
<Box>
|
||||
<Text fw={600} fz="lg" mb={4}>
|
||||
<Title order={2} mb={8} style={{ lineHeight: 1.2 }}>
|
||||
Kualifikasi
|
||||
</Text>
|
||||
</Title>
|
||||
<Text
|
||||
fz="sm"
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={1.6}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Button, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconBriefcase, IconClock, IconMapPin, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -53,17 +53,19 @@ function Page() {
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} pb={80}>
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Title order={1} ta="center" c={colors["blue-button"]} fw="bold" lh={1.15}>
|
||||
Lowongan Kerja Lokal
|
||||
</Text>
|
||||
</Title>
|
||||
<Group justify='center'>
|
||||
<TextInput
|
||||
radius={'xl'}
|
||||
w={{ base: 500, md: 700 }}
|
||||
w={{ base: '100%', md: 700 }}
|
||||
placeholder='Cari Pekerjaan'
|
||||
leftSection={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
@@ -80,30 +82,42 @@ function Page() {
|
||||
<Paper key={k} p={'xl'}>
|
||||
<Stack gap={'md'}>
|
||||
<Box>
|
||||
<Flex gap={'xl'} align={'center'}>
|
||||
<IconBriefcase color={colors['blue-button']} size={50} />
|
||||
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
|
||||
<IconBriefcase color={colors['blue-button']} size={40} />
|
||||
<Box>
|
||||
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>{v.posisi}</Text>
|
||||
<Text fz={'h4'}>{v.namaPerusahaan}</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'lg', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
|
||||
{v.posisi}
|
||||
</Text>
|
||||
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
|
||||
{v.namaPerusahaan}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={'xl'} align={'center'}>
|
||||
<IconMapPin color={colors['blue-button']} size={50} />
|
||||
<Text fz={'h4'}>{v.lokasi}</Text>
|
||||
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
|
||||
<IconMapPin color={colors['blue-button']} size={40} />
|
||||
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
|
||||
{v.lokasi}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={'xl'} align={'center'}>
|
||||
<IconClock color={colors['blue-button']} size={50} />
|
||||
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
|
||||
<IconClock color={colors['blue-button']} size={40} />
|
||||
<Box>
|
||||
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>Full Time</Text>
|
||||
<Text fz={'h4'}>{formatCurrency(v.gaji)}</Text>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
|
||||
Full Time
|
||||
</Text>
|
||||
<Text fz={{ base: 'sm', md: 'h4' }} lh={1.5}>
|
||||
{formatCurrency(v.gaji)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)}>Detail</Button>
|
||||
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Detail
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
@@ -123,4 +137,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core';
|
||||
import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
|
||||
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider, Title } from '@mantine/core';
|
||||
import { IconArrowBack, IconBrandWhatsapp, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -31,14 +31,16 @@ function DetailProdukPasarUser() {
|
||||
<Box py={20}>
|
||||
{/* Tombol kembali */}
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali ke daftar produk
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
<Text fz={{ base: 'md', md: 'lg' }} lh={1.5}>
|
||||
Kembali ke daftar produk
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper
|
||||
@@ -65,26 +67,31 @@ function DetailProdukPasarUser() {
|
||||
<Box
|
||||
h={300}
|
||||
bg="gray.1"
|
||||
display="flex"
|
||||
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 'var(--mantine-radius-md)' }}
|
||||
>
|
||||
<Text c="dimmed">Tidak ada gambar</Text>
|
||||
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Tidak ada gambar
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Detail Produk */}
|
||||
<Stack gap="xs">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
<Title order={2} lh={1.1} c={colors['blue-button']}>
|
||||
{data.nama || 'Produk Tanpa Nama'}
|
||||
</Text>
|
||||
</Title>
|
||||
<Group>
|
||||
<Badge color="green" size="lg" radius="md">
|
||||
Rp {data.harga?.toLocaleString('id-ID')}
|
||||
<Text c={"white"} fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
|
||||
Rp {data.harga?.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Badge>
|
||||
{data.rating && (
|
||||
<Group gap={4}>
|
||||
<IconStar size={18} color="#FFD43B" />
|
||||
<Text fz="md" fw={500}>{data.rating}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} lh={1.5}>
|
||||
{data.rating}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
@@ -95,16 +102,20 @@ function DetailProdukPasarUser() {
|
||||
{/* Info Tambahan */}
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw={600}>Kategori</Text>
|
||||
<Title order={3} lh={1.15}>
|
||||
Kategori
|
||||
</Title>
|
||||
<Group gap="xs" mt={4}>
|
||||
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
|
||||
data.KategoriToPasar.map((kategori) => (
|
||||
<Badge key={kategori.id} color="blue" variant="light">
|
||||
<Badge key={kategori.id} color="blue" variant="light" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
|
||||
{kategori.kategori.nama}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.5}>
|
||||
Tidak ada kategori
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
@@ -112,14 +123,18 @@ function DetailProdukPasarUser() {
|
||||
{data.alamatUsaha && (
|
||||
<Group gap={6}>
|
||||
<IconMapPin size={18} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.alamatUsaha}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
{data.alamatUsaha}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{data.kontak && (
|
||||
<Group gap={6}>
|
||||
<IconPhone size={18} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.kontak}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
{data.kontak}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -128,8 +143,10 @@ function DetailProdukPasarUser() {
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
|
||||
<Text fz="md" c="dimmed" mt={4}>
|
||||
<Title order={3} lh={1.15}>
|
||||
Deskripsi Produk
|
||||
</Title>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed" mt={4} lh={1.5}>
|
||||
Tidak ada deskripsi.
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -144,8 +161,11 @@ function DetailProdukPasarUser() {
|
||||
component="a"
|
||||
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
|
||||
target="_blank"
|
||||
leftSection={<IconBrandWhatsapp/>}
|
||||
>
|
||||
Hubungi Penjual via WhatsApp
|
||||
<Text c={"white"} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Hubungi Penjual via WhatsApp
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -154,4 +174,4 @@ function DetailProdukPasarUser() {
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailProdukPasarUser;
|
||||
export default DetailProdukPasarUser;
|
||||
@@ -7,7 +7,7 @@ import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from
|
||||
import { motion } from 'motion/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
function Page() {
|
||||
@@ -30,8 +30,8 @@ function Page() {
|
||||
|
||||
const filteredData = selectedCategory
|
||||
? data?.filter(item =>
|
||||
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
|
||||
)
|
||||
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
|
||||
)
|
||||
: data;
|
||||
|
||||
useShallowEffect(() => {
|
||||
@@ -55,7 +55,7 @@ function Page() {
|
||||
<Box>
|
||||
<Grid align="center" px={{ base: 'md', md: 100 }}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Title order={1} c={colors["blue-button"]} fw="bold">
|
||||
<Title order={1} c={colors["blue-button"]} fw="bold" lh={1.15}>
|
||||
Pasar Desa
|
||||
</Title>
|
||||
</GridCol>
|
||||
@@ -71,7 +71,14 @@ function Page() {
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Text px={{ base: 'md', md: 100 }} pt={20} ta="justify" fz={{ base: 'sm', md: 'md' }}>
|
||||
<Text
|
||||
px={{ base: 'md', md: 100 }}
|
||||
pt={20}
|
||||
ta="justify"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.55 }}
|
||||
c="black"
|
||||
>
|
||||
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -92,6 +99,9 @@ function Page() {
|
||||
searchable
|
||||
nothingFoundMessage="Tidak ada kategori ditemukan"
|
||||
style={{ width: '100%' }}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.55 }}
|
||||
c="black"
|
||||
/>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
@@ -114,15 +124,29 @@ function Page() {
|
||||
style={{ objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text py="sm" fw="bold" fz={{ base: 'md', md: 'lg' }}>
|
||||
<Text
|
||||
py="sm"
|
||||
fw="bold"
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={{ base: 1.3, md: 1.25 }}
|
||||
c="black"
|
||||
>
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.55 }}
|
||||
c="black"
|
||||
>
|
||||
Rp {v.harga.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
<Flex py="sm" gap="md">
|
||||
<Flex py="sm" gap="md" align="center">
|
||||
<IconStarFilled size={20} color="#EBCB09" />
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={{ base: 1.4, md: 1.45 }}
|
||||
c="black"
|
||||
>
|
||||
{v.rating}
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -130,7 +154,11 @@ function Page() {
|
||||
<Box>
|
||||
<Flex gap="md" align="center">
|
||||
<IconMapPinFilled size={20} color="red" />
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={{ base: 1.4, md: 1.45 }}
|
||||
c="black"
|
||||
>
|
||||
{v.alamatUsaha}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -69,48 +69,47 @@ function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py={{ base: 'xl', md: 'xl' }} gap={'22'}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Grid align='center'>
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Title
|
||||
order={1}
|
||||
c={colors["blue-button"]}
|
||||
fw={"bold"}
|
||||
fz={{ base: '28px', md: '32px' }}
|
||||
lh={{ base: '1.2', md: '1.25' }}
|
||||
fw="bold"
|
||||
lh={{ base: 1.2, md: 1.2 }}
|
||||
>
|
||||
Program Kemiskinan
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
radius={"lg"}
|
||||
placeholder='Cari Program'
|
||||
radius="lg"
|
||||
placeholder="Cari Program"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
w="100%"
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text
|
||||
fz={{ base: '14px', md: '16px' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
c="black"
|
||||
ta={{ base: 'left', md: 'left' }}
|
||||
pt={20}
|
||||
ta="left"
|
||||
pt={{ base: 'sm', md: 20 }}
|
||||
>
|
||||
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Stack gap={'lg'} justify="center">
|
||||
<SimpleGrid
|
||||
pb={10}
|
||||
pb={{ base: 'md', md: 10 }}
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2
|
||||
@@ -118,20 +117,19 @@ function Page() {
|
||||
>
|
||||
{state.findMany.data.map(v => {
|
||||
return (
|
||||
<Paper p={'xl'} key={v.id}>
|
||||
<Paper p={{ base: 'lg', md: 'xl' }} key={v.id}>
|
||||
<Title
|
||||
order={3}
|
||||
fw={'bold'}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: '18px', md: '20px' }}
|
||||
lh={{ base: '1.3', md: '1.35' }}
|
||||
lh={{ base: 1.2, md: 1.2 }}
|
||||
>
|
||||
{v.nama}
|
||||
</Title>
|
||||
<Text
|
||||
fz={{ base: '14px', md: '16px' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
c={'black'}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
c="black"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
/>
|
||||
@@ -139,7 +137,7 @@ function Page() {
|
||||
)
|
||||
})}
|
||||
</SimpleGrid>
|
||||
<Center my={10}>
|
||||
<Center my={{ base: 'md', md: 10 }}>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
@@ -147,16 +145,15 @@ function Page() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}}
|
||||
total={totalPages}
|
||||
my={"md"}
|
||||
my="md"
|
||||
/>
|
||||
</Center>
|
||||
<Paper p={'xl'}>
|
||||
<Paper p={{ base: 'lg', md: 'xl' }}>
|
||||
<Title
|
||||
order={3}
|
||||
fw={'bold'}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: '18px', md: '20px' }}
|
||||
lh={{ base: '1.3', md: '1.35' }}
|
||||
lh={{ base: 1.2, md: 1.2 }}
|
||||
mb="md"
|
||||
>
|
||||
Statistik Kemiskinan Masyarakat
|
||||
@@ -166,7 +163,7 @@ function Page() {
|
||||
<Box w="100%" style={{ overflowX: 'auto' }}>
|
||||
<Center>
|
||||
<RechartsLineChart
|
||||
width={Math.min(800, window.innerWidth - 100)}
|
||||
width={Math.min(800, typeof window !== 'undefined' ? window.innerWidth - 100 : 800)}
|
||||
height={400}
|
||||
data={statistikData}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
@@ -175,10 +172,12 @@ function Page() {
|
||||
<XAxis
|
||||
dataKey="tahun"
|
||||
label={{ value: 'Tahun', position: 'insideBottomRight', offset: -5 }}
|
||||
tick={{ fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
label={{ value: 'Jumlah', angle: -90, position: 'insideLeft' }}
|
||||
domain={[0, 'auto']}
|
||||
tick={{ fontSize: 12 }}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => [`${value} orang`, 'Jumlah']}
|
||||
@@ -199,9 +198,9 @@ function Page() {
|
||||
) : (
|
||||
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
|
||||
<Text
|
||||
fz={{ base: '12px', md: '14px' }}
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
lh={{ base: '1.4', md: '1.5' }}
|
||||
lh={{ base: 1.4, md: 1.4 }}
|
||||
>
|
||||
{state.findMany.loading
|
||||
? 'Memuat data statistik...'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Text, Paper, Skeleton, Center } from '@mantine/core';
|
||||
import { Stack, Box, Text, Paper, Skeleton, Center, Title } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { BarChart } from '@mantine/charts';
|
||||
@@ -28,16 +28,15 @@ function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
// Add this check before the return statement
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||
<Title order={1} c={colors['blue-button']} fw="bold">
|
||||
Sektor Unggulan Desa Darmasaba
|
||||
</Text>
|
||||
<Text c="dimmed" mt="md">
|
||||
</Title>
|
||||
<Text c="dimmed" mt="md" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Data sektor unggulan belum tersedia
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -53,32 +52,49 @@ function Page() {
|
||||
Ton: item.value,
|
||||
}));
|
||||
|
||||
const chartWidth = Math.max(600, chartData.length * 150); // contoh: 150px per bar
|
||||
const chartWidth = Math.max(600, chartData.length * 150);
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title ta="center" order={1} c={colors['blue-button']} fw="bold">
|
||||
Sektor Unggulan Desa Darmasaba
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
mt="sm"
|
||||
>
|
||||
Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan
|
||||
</Text>
|
||||
<Text ta={'center'} fz={'h4'}> Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap="lg" justify="center">
|
||||
{data.map((v, k) => {
|
||||
return (
|
||||
<Paper p={'xl'} key={k}>
|
||||
<Text fw={'bold'} fz={'h4'}>{v.name}</Text>
|
||||
<Text fz={'h4'} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
|
||||
<Paper p="xl" key={k}>
|
||||
<Title order={3} fw="bold">
|
||||
{v.name}
|
||||
</Title>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: v.description || '' }}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
})}
|
||||
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
||||
<Paper p="xl">
|
||||
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
|
||||
<Title order={3} fw="bold" pb="md">
|
||||
Statistik Sektor Unggulan Darmasaba
|
||||
</Title>
|
||||
<Box style={{ width: '100%', overflowX: 'auto', maxWidth: `${chartWidth}px` }}>
|
||||
<Center>
|
||||
<BarChart
|
||||
@@ -98,7 +114,7 @@ function Page() {
|
||||
yAxisLabel="Ton"
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '12px', // ukuran font lebih kecil di mobile
|
||||
fontSize: '12px',
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
@@ -111,4 +127,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -0,0 +1,174 @@
|
||||
'use client';
|
||||
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function DetailPegawaiBumdes() {
|
||||
const statePegawai = useProxy(stateStrukturBumDes.pegawai);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateStrukturBumDes.posisiOrganisasi.findMany.load();
|
||||
statePegawai.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
if (!statePegawai.findUnique.data) {
|
||||
return (
|
||||
<Stack py="lg">
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = statePegawai.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
{/* Back button */}
|
||||
<Group mb="lg" px={{ base: 'md', md: 100 }}>
|
||||
<Box
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<IconArrowBack size={22} color={colors['blue-button']} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh="1.4" fw={500} c={colors['blue-button']}>
|
||||
Kembali
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
mx="auto"
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{ border: '1px solid #eaeaea' }}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
{/* Foto Profil */}
|
||||
<Image
|
||||
src={data.image?.link || '/placeholder-profile.png'}
|
||||
alt={data.namaLengkap || 'Foto Profil'}
|
||||
w={160}
|
||||
h={160}
|
||||
radius={100}
|
||||
fit="cover"
|
||||
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Nama & Jabatan */}
|
||||
<Stack align="center" gap={2}>
|
||||
<Title
|
||||
order={2}
|
||||
c={colors['blue-button']}
|
||||
fw={700}
|
||||
fz={{ base: 'xl', md: '28px' }}
|
||||
lh="1.2"
|
||||
ta="center"
|
||||
>
|
||||
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||
</Title>
|
||||
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh="1.4"
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
>
|
||||
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
{/* Informasi Detail */}
|
||||
<Stack gap="md">
|
||||
<InfoRow label="Email" value={data.email} />
|
||||
<InfoRow label="Telepon" value={data.telepon} />
|
||||
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||
<InfoRow
|
||||
label="Tanggal Masuk"
|
||||
value={
|
||||
data.tanggalMasuk
|
||||
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Status"
|
||||
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||
valueColor={data.isActive ? 'green' : 'red'}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/* Komponen Baris Informasi */
|
||||
function InfoRow({
|
||||
label,
|
||||
value,
|
||||
valueColor,
|
||||
multiline = false,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
valueColor?: string;
|
||||
multiline?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
fw={600}
|
||||
lh="1.3"
|
||||
c="dark"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh="1.5"
|
||||
c={valueColor || 'dimmed'}
|
||||
style={{
|
||||
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{value || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPegawaiBumdes;
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
|
||||
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
@@ -32,12 +31,13 @@ import {
|
||||
IconZoomOut,
|
||||
} from '@tabler/icons-react'
|
||||
import { debounce } from 'lodash'
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
import '../../ppid/struktur-ppid/struktur.css'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -49,14 +49,16 @@ export default function Page() {
|
||||
paddingBottom: 48,
|
||||
}}
|
||||
>
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
|
||||
<BackButton />
|
||||
|
||||
<Stack align="center" gap="xl" mt="xl">
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36 }}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
lh={{ base: 1.05, md: 1.03 }}
|
||||
>
|
||||
Struktur Organisasi & SK Pengurus BumDes
|
||||
</Title>
|
||||
@@ -75,14 +77,18 @@ export default function Page() {
|
||||
}
|
||||
|
||||
function StrukturOrganisasiBumDes() {
|
||||
const router = useTransitionRouter()
|
||||
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
||||
const router = useTransitionRouter()
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// debounce pencarian
|
||||
const debouncedSearch = useRef(
|
||||
debounce((value: string) => setSearchQuery(value), 1000)
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value)
|
||||
}, 1000)
|
||||
).current
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,8 +96,7 @@ function StrukturOrganisasiBumDes() {
|
||||
}, [])
|
||||
|
||||
const isLoading =
|
||||
!stateOrganisasi.findMany.data &&
|
||||
stateOrganisasi.findMany.loading !== false
|
||||
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -149,7 +154,7 @@ function StrukturOrganisasiBumDes() {
|
||||
)
|
||||
}
|
||||
|
||||
// 📊 susun struktur organisasi
|
||||
// 🧩 buat struktur organisasi
|
||||
const posisiMap = new Map<string, any>()
|
||||
const aktifPegawai = data.filter((p: any) => p.isActive)
|
||||
|
||||
@@ -183,7 +188,6 @@ function StrukturOrganisasiBumDes() {
|
||||
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
||||
title: node.nama || 'Tanpa Jabatan',
|
||||
image: pegawai?.image?.link || '/img/default.png',
|
||||
description: node.deskripsi || '',
|
||||
},
|
||||
children: node.children?.map(toOrgChartFormat) || [],
|
||||
}
|
||||
@@ -208,7 +212,7 @@ function StrukturOrganisasiBumDes() {
|
||||
chartData = filterNodes(chartData)
|
||||
}
|
||||
|
||||
// 🔍 fullscreen dan zoom control
|
||||
// 🎬 fullscreen & zoom control
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
chartContainerRef.current?.requestFullscreen()
|
||||
@@ -225,7 +229,7 @@ function StrukturOrganisasiBumDes() {
|
||||
|
||||
return (
|
||||
<Stack align="center" mt="xl">
|
||||
{/* 🧭 Kontrol atas */}
|
||||
{/* 🔍 Controls */}
|
||||
<Paper
|
||||
shadow="xs"
|
||||
w={{
|
||||
@@ -244,6 +248,7 @@ function StrukturOrganisasiBumDes() {
|
||||
overflowX: 'auto' // ⬅️ untuk mencegah overflow
|
||||
}}
|
||||
>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Group justify='center'>
|
||||
<TextInput
|
||||
@@ -360,165 +365,163 @@ function StrukturOrganisasiBumDes() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* 🧩 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
{/* 🧩 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Box style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
|
||||
minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
|
||||
}}>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
|
||||
className="p-organizationchart p-organizationchart-horizontal"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeCard({ node, router }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const hasId = Boolean(node?.data?.id)
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={300}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
style={{
|
||||
...styles,
|
||||
width: '100%',
|
||||
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
|
||||
minHeight: isMobile ? 240 : 280,
|
||||
padding: isMobile ? 16 : 20,
|
||||
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
|
||||
borderColor: 'rgba(28, 110, 164, 0.3)',
|
||||
borderWidth: 2,
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: hasId ? 'pointer' : 'default',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap={12}>
|
||||
{/* Photo */}
|
||||
<Box
|
||||
style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
width={96}
|
||||
height={96}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
fw={700}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
fz={{ base: 13, md: 15 }}
|
||||
lh={1.2}
|
||||
style={{
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
{/* Title/Position */}
|
||||
<Text
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
fw={500}
|
||||
lineClamp={2}
|
||||
fz={{ base: 12, md: 13 }}
|
||||
lh={1.3}
|
||||
style={{
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* Detail Button */}
|
||||
{hasId && (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
size="xs"
|
||||
fullWidth
|
||||
mt={8}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/${node.data.id}`)
|
||||
}
|
||||
style={{
|
||||
height: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Box style={{
|
||||
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
|
||||
minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
|
||||
}}>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
|
||||
className="p-organizationchart p-organizationchart-horizontal"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeCard({ node, router }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const hasId = Boolean(node?.data?.id)
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={300}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
|
||||
style={{
|
||||
...styles,
|
||||
width: '100%',
|
||||
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
|
||||
minHeight: isMobile ? 240 : 280,
|
||||
padding: isMobile ? 16 : 20,
|
||||
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
|
||||
borderColor: 'rgba(28, 110, 164, 0.3)',
|
||||
borderWidth: 2,
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: hasId ? 'pointer' : 'default',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap={12}>
|
||||
{/* Photo */}
|
||||
<Box
|
||||
style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
width={96}
|
||||
height={96}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
fw={700}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
fz={{ base: 13, md: 15 }}
|
||||
lh={1.2}
|
||||
style={{
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
{/* Title/Position */}
|
||||
<Text
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
fw={500}
|
||||
lineClamp={2}
|
||||
fz={{ base: 12, md: 13 }}
|
||||
lh={1.3}
|
||||
style={{
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* Detail Button */}
|
||||
{hasId && (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
size="xs"
|
||||
fullWidth
|
||||
mt={8}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/ppid/struktur-ppid/${node.data.id}`)
|
||||
}
|
||||
style={{
|
||||
height: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
<Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box } from '@mantine/core';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import React, { Suspense } from 'react';
|
||||
import LayoutTabs from './_lib/layoutTabs';
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box bg={colors.Bg}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<LayoutTabs>
|
||||
|
||||
@@ -517,7 +517,7 @@ function NodeCard({ node, router }: any) {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||
<Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -216,105 +216,282 @@ export default function ModernNewsNotification({
|
||||
{/* Widget Panel */}
|
||||
<Transition mounted={widgetOpen} transition="slide-up" duration={300}>
|
||||
{(styles) => (
|
||||
<Paper
|
||||
style={{
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
left: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
maxHeight: "500px",
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
|
||||
borderRadius: "16px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)",
|
||||
padding: "16px 20px",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconBell size={20} />
|
||||
<Text c="white" fw={600} size="md">
|
||||
Berita & Pengumuman
|
||||
</Text>
|
||||
</Group>
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
setWidgetOpen(false);
|
||||
onSeen?.();
|
||||
<Box>
|
||||
<Box hiddenFrom="md">
|
||||
<Paper
|
||||
style={{
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
left: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
maxHeight: "500px",
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
|
||||
borderRadius: "16px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)",
|
||||
padding: "16px 20px",
|
||||
color: "white",
|
||||
}}
|
||||
variant="transparent"
|
||||
c="white"
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
|
||||
{news.length === 0 ? (
|
||||
<Box p="xl" style={{ textAlign: "center" }}>
|
||||
<Text c="dimmed" size="sm">
|
||||
Tidak ada berita terbaru
|
||||
</Text>
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconBell size={20} />
|
||||
<Text c="white" fw={600} size="md">
|
||||
Berita & Pengumuman
|
||||
</Text>
|
||||
</Group>
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
setWidgetOpen(false);
|
||||
onSeen?.();
|
||||
}}
|
||||
variant="transparent"
|
||||
c="white"
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{news.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
border: "1px solid #e9ecef",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#1e5a7e";
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "#e9ecef";
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
onClick={() => handleNotificationClick(item)}
|
||||
>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Badge
|
||||
size="sm"
|
||||
color={item.type === "berita" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
|
||||
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
|
||||
{news.length === 0 ? (
|
||||
<Box p="xl" style={{ textAlign: "center" }}>
|
||||
<Text c="dimmed" size="sm">
|
||||
Tidak ada berita terbaru
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{news.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
border: "1px solid #e9ecef",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#1e5a7e";
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "#e9ecef";
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
onClick={() => handleNotificationClick(item)}
|
||||
>
|
||||
{item.type === "berita" ? "Berita" : "Pengumuman"}
|
||||
</Badge>
|
||||
<IconChevronRight size={16} color="#adb5bd" />
|
||||
</Group>
|
||||
<Text fw={600} size="sm" mb={4} lineClamp={2}>
|
||||
{item.title || "Tanpa Judul"}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
{stripHtml(item.content).substring(0, 100)}...
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Badge
|
||||
size="sm"
|
||||
color={item.type === "berita" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
>
|
||||
{item.type === "berita" ? "Berita" : "Pengumuman"}
|
||||
</Badge>
|
||||
<IconChevronRight size={16} color="#adb5bd" />
|
||||
</Group>
|
||||
<Text fw={600} size="sm" mb={4} lineClamp={2}>
|
||||
{item.title || "Tanpa Judul"}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
{stripHtml(item.content).substring(0, 100)}...
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Box visibleFrom="md">
|
||||
<Paper
|
||||
style={{
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
right: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
maxHeight: "500px",
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
|
||||
borderRadius: "16px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)",
|
||||
padding: "16px 20px",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconBell size={20} />
|
||||
<Text c="white" fw={600} size="md">
|
||||
Berita & Pengumuman
|
||||
</Text>
|
||||
</Group>
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
setWidgetOpen(false);
|
||||
onSeen?.();
|
||||
}}
|
||||
variant="transparent"
|
||||
c="white"
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
|
||||
{news.length === 0 ? (
|
||||
<Box p="xl" style={{ textAlign: "center" }}>
|
||||
<Text c="dimmed" size="sm">
|
||||
Tidak ada berita terbaru
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{news.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
border: "1px solid #e9ecef",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#1e5a7e";
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "#e9ecef";
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
onClick={() => handleNotificationClick(item)}
|
||||
>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Badge
|
||||
size="sm"
|
||||
color={item.type === "berita" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
>
|
||||
{item.type === "berita" ? "Berita" : "Pengumuman"}
|
||||
</Badge>
|
||||
<IconChevronRight size={16} color="#adb5bd" />
|
||||
</Group>
|
||||
<Text fw={600} size="sm" mb={4} lineClamp={2}>
|
||||
{item.title || "Tanpa Judul"}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
{stripHtml(item.content).substring(0, 100)}...
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
{/* Toast Notification */}
|
||||
<Transition
|
||||
<Box>
|
||||
<Box hiddenFrom="md">
|
||||
<Transition
|
||||
mounted={toastVisible && !!currentNews}
|
||||
transition="slide-left"
|
||||
duration={300}
|
||||
>
|
||||
{(styles) => (
|
||||
<Paper
|
||||
style={{
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
left: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
border: "1px solid #e9ecef",
|
||||
}}
|
||||
onClick={handleLihatSemua}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
height: "3px",
|
||||
background: "#1e5a7e",
|
||||
animation: "shrink 8s linear forwards",
|
||||
}}
|
||||
/>
|
||||
<style>{`
|
||||
@keyframes shrink {
|
||||
from { width: 100%; }
|
||||
to { width: 0%; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<Box p="md">
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Badge
|
||||
size="md"
|
||||
color={currentNews?.type === "berita" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
>
|
||||
{currentNews?.type === "berita"
|
||||
? "Berita Terbaru"
|
||||
: "Pengumuman"}
|
||||
</Badge>
|
||||
<CloseButton onClick={handleDismissToast} size="sm" />
|
||||
</Group>
|
||||
|
||||
<Text fw={600} size="sm" mb={6}>
|
||||
{currentNews?.title || "Informasi Terbaru"}
|
||||
</Text>
|
||||
|
||||
<Text size="xs" c="dimmed" lineClamp={3}>
|
||||
{stripHtml(currentNews?.content || "")}
|
||||
</Text>
|
||||
|
||||
<Group justify="space-between" mt="md">
|
||||
<Text size="xs" c="dimmed">
|
||||
{news.length > 1 ? `${news.length} berita tersedia` : "1 berita"}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
fw={500}
|
||||
c="#1e5a7e"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={handleLihatSemua}
|
||||
>
|
||||
Lihat Semua →
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Transition>
|
||||
</Box>
|
||||
<Box visibleFrom="md">
|
||||
<Transition
|
||||
mounted={toastVisible && !!currentNews}
|
||||
transition="slide-left"
|
||||
transition="slide-right"
|
||||
duration={300}
|
||||
>
|
||||
{(styles) => (
|
||||
@@ -323,7 +500,7 @@ export default function ModernNewsNotification({
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
left: "24px",
|
||||
right: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
|
||||
@@ -387,6 +564,8 @@ export default function ModernNewsNotification({
|
||||
</Paper>
|
||||
)}
|
||||
</Transition>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,18 +3,17 @@
|
||||
import colors from "@/con/colors"
|
||||
import stateNav from "@/state/state-nav"
|
||||
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
|
||||
import { IconSearch, IconUser } from "@tabler/icons-react"
|
||||
import { IconSearch, IconUserCog } from "@tabler/icons-react"
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { MenuItem } from "../../../../types/menu-item"
|
||||
import { NavbarSearch } from "./NavBarSearch"
|
||||
import { NavbarSubMenu } from "./NavbarSubMenu"
|
||||
import { authStore } from "@/store/authStore";
|
||||
|
||||
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
|
||||
const stateAuth = {
|
||||
role: "admin", // coba ubah ke "user" buat test
|
||||
}
|
||||
const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
|
||||
|
||||
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
const { item, isSearch } = useSnapshot(stateNav)
|
||||
@@ -70,8 +69,8 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
</Tooltip>
|
||||
|
||||
{/* hanya tampil kalau role = admin */}
|
||||
{stateAuth.role === "admin" && (
|
||||
<Tooltip label="Profil Saya" position="bottom" withArrow>
|
||||
{isAdmin && (
|
||||
<Tooltip label="Kembali ke Admin" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
next.push("/admin/landing-page/profil/program-inovasi")
|
||||
@@ -80,7 +79,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
radius="xl"
|
||||
variant="light"
|
||||
>
|
||||
<IconUser size={22} />
|
||||
<IconUserCog size={22} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -10,10 +10,19 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Progress,
|
||||
Group,
|
||||
} from '@mantine/core';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
||||
import { authStore } from '@/store/authStore';
|
||||
|
||||
// ⚙️ Configuration
|
||||
const CONFIG = {
|
||||
POLL_INTERVAL: 3000, // 3 detik
|
||||
MAX_RETRIES: 2, // 2x retry
|
||||
TIMEOUT_DURATION: 5 * 60 * 1000, // 5 menit (300 detik)
|
||||
};
|
||||
|
||||
async function fetchUser() {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
@@ -26,21 +35,48 @@ async function fetchUser() {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function WaitingRoom() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
// ⏱️ Countdown timer
|
||||
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
|
||||
const [hasTimedOut, setHasTimedOut] = useState(false);
|
||||
|
||||
// ⏱️ Countdown effect
|
||||
useEffect(() => {
|
||||
if (isRedirecting || hasTimedOut) return;
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
setTimeLeft((prev) => {
|
||||
if (prev <= 1) {
|
||||
setHasTimedOut(true);
|
||||
setError('Waktu tunggu habis. Silakan hubungi administrator atau coba login ulang nanti.');
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(countdownInterval);
|
||||
}, [isRedirecting, hasTimedOut]);
|
||||
|
||||
// 🔄 Polling effect
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
const poll = async () => {
|
||||
if (isRedirecting || !isMounted) return;
|
||||
if (isRedirecting || !isMounted || hasTimedOut) return;
|
||||
|
||||
try {
|
||||
const data = await fetchUser();
|
||||
@@ -59,12 +95,11 @@ export default function WaitingRoom() {
|
||||
});
|
||||
}
|
||||
|
||||
// In the poll function
|
||||
// ✅ Check if approved
|
||||
if (currentUser?.isActive === true) {
|
||||
setIsRedirecting(true);
|
||||
clearInterval(interval);
|
||||
|
||||
// Update authStore with the current user data
|
||||
authStore.setUser({
|
||||
id: currentUser.id,
|
||||
name: currentUser.name || 'User',
|
||||
@@ -78,7 +113,7 @@ export default function WaitingRoom() {
|
||||
localStorage.removeItem('auth_nomor');
|
||||
localStorage.removeItem('auth_username');
|
||||
|
||||
// Force a session refresh
|
||||
// Force session refresh
|
||||
try {
|
||||
const res = await fetch('/api/auth/refresh-session', {
|
||||
method: 'POST',
|
||||
@@ -99,26 +134,26 @@ export default function WaitingRoom() {
|
||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
break;
|
||||
}
|
||||
window.location.href = redirectPath; // Use window.location to force full page reload
|
||||
window.location.href = redirectPath;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing session:', error);
|
||||
router.refresh(); // Fallback to client-side refresh
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!isMounted) return;
|
||||
|
||||
if (err.message.includes('401')) {
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
if (retryCount < CONFIG.MAX_RETRIES) {
|
||||
setRetryCount((prev) => prev + 1);
|
||||
setTimeout(() => {
|
||||
if (isMounted) interval = setInterval(poll, 3000);
|
||||
if (isMounted) interval = setInterval(poll, CONFIG.POLL_INTERVAL);
|
||||
}, 800);
|
||||
} else {
|
||||
setError('Sesi tidak valid. Silakan login ulang.');
|
||||
clearInterval(interval);
|
||||
authStore.setUser(null); // ✅ clear sesi
|
||||
authStore.setUser(null);
|
||||
}
|
||||
} else {
|
||||
console.error('Error polling:', err);
|
||||
@@ -126,26 +161,53 @@ export default function WaitingRoom() {
|
||||
}
|
||||
};
|
||||
|
||||
interval = setInterval(poll, 3000);
|
||||
interval = setInterval(poll, CONFIG.POLL_INTERVAL);
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [router, isRedirecting, retryCount]);
|
||||
}, [router, isRedirecting, retryCount, hasTimedOut]);
|
||||
|
||||
// ✅ UI Error
|
||||
if (error) {
|
||||
// 🚨 Handle logout
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
} finally {
|
||||
authStore.setUser(null);
|
||||
localStorage.clear();
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
// ❌ UI Error / Timeout
|
||||
if (error || hasTimedOut) {
|
||||
return (
|
||||
<Center h="100vh">
|
||||
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}>
|
||||
<Center h="100vh" bg={colors.Bg}>
|
||||
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||
<Stack align="center" gap="md">
|
||||
<Title order={3} c="red">
|
||||
Sesi Tidak Valid
|
||||
<Title order={3} c="red" ta="center">
|
||||
{hasTimedOut ? '⏱️ Waktu Habis' : '❌ Sesi Tidak Valid'}
|
||||
</Title>
|
||||
<Text>{error}</Text>
|
||||
<Button onClick={() => router.push('/login')}>
|
||||
Login Ulang
|
||||
</Button>
|
||||
<Text ta="center" size="sm">
|
||||
{error || 'Waktu tunggu persetujuan telah habis.'}
|
||||
</Text>
|
||||
<Text ta="center" size="xs" c="dimmed">
|
||||
Silakan hubungi Superadmin atau coba login ulang nanti.
|
||||
</Text>
|
||||
<Group gap="sm" w="100%">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Kembali ke Login
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
@@ -171,24 +233,56 @@ export default function WaitingRoom() {
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN!
|
||||
// ⏳ UI Default (MENUNGGU)
|
||||
const progressValue = ((CONFIG.TIMEOUT_DURATION / 1000 - timeLeft) / (CONFIG.TIMEOUT_DURATION / 1000)) * 100;
|
||||
|
||||
return (
|
||||
<Center h="100vh" bg={colors.Bg}>
|
||||
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||
<Stack align="center" gap="lg">
|
||||
<Title order={2} c={colors['blue-button']} ta="center">
|
||||
Menunggu Persetujuan
|
||||
⏳ Menunggu Persetujuan
|
||||
</Title>
|
||||
|
||||
<Text ta="center" c="dimmed">
|
||||
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||
</Text>
|
||||
<Text ta="center" size="sm" c="dimmed">
|
||||
|
||||
<Text ta="center" size="sm" fw={500}>
|
||||
Nomor: {user?.nomor || '...'}
|
||||
</Text>
|
||||
|
||||
{/* ⏱️ Countdown Timer */}
|
||||
<Stack w="100%" gap="xs">
|
||||
<Group justify="space-between" w="100%">
|
||||
<Text size="sm" c="dimmed">Sisa waktu:</Text>
|
||||
<Text size="sm" fw={600} c={timeLeft < 60 ? 'red' : colors['blue-button']}>
|
||||
{formatTime(timeLeft)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={progressValue}
|
||||
color={timeLeft < 60 ? 'red' : colors['blue-button']}
|
||||
size="sm"
|
||||
animated
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Loader size="sm" color={colors['blue-button']} />
|
||||
|
||||
<Text ta="center" size="xs" c="dimmed">
|
||||
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||
</Text>
|
||||
|
||||
{/* 🚪 Tombol Keluar */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={handleLogout}
|
||||
c="dimmed"
|
||||
>
|
||||
Keluar dari Halaman Ini
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
|
||||
Reference in New Issue
Block a user