Fix UI Admin Menu Landing Page & PPID

This commit is contained in:
2025-09-01 16:14:28 +08:00
parent 22ec8d942d
commit 7ae83788b4
74 changed files with 4312 additions and 2798 deletions

View File

@@ -1,9 +1,9 @@
'use client'
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -73,28 +73,44 @@ function EditDaftarInformasiPublik() {
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Daftar Informasi Publik Desa Darmasaba</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Daftar Informasi Publik
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xl">
<TextInput
label="Jenis Informasi"
placeholder="Masukkan jenis informasi"
value={formData.jenisInformasi}
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => {
setFormData({
...formData,
jenisInformasi: val.target.value
})
});
}}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -103,19 +119,35 @@ function EditDaftarInformasiPublik() {
}}
/>
</Box>
<TextInput
type='date'
type="date"
label="Tanggal Publikasi"
placeholder="Pilih tanggal publikasi"
value={formatDateForInput(formData.tanggal)}
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
placeholder="masukkan tanggal publikasi"
onChange={(val) => {
setFormData({
...formData,
tanggal: val.target.value
})
});
}}
required
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan Perubahan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,13 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import daftarInformasiPublik from '../../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import { useShallowEffect } from '@mantine/hooks';
function DetailDaftarInformasiPublik() {
const [modalHapus, setModalHapus] = useState(false)
@@ -31,76 +31,105 @@ function DetailDaftarInformasiPublik() {
if (!stateDaftarInformasi.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = stateDaftarInformasi.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Berita</Text>
{stateDaftarInformasi.findUnique.data ? (
<Paper key={stateDaftarInformasi.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Informasi</Text>
<Text fz={"lg"}>{stateDaftarInformasi.findUnique.data?.jenisInformasi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text>
<Text fz={"lg"}>{stateDaftarInformasi.findUnique.data?.tanggal
? new Date(stateDaftarInformasi.findUnique.data.tanggal).toLocaleDateString()
: "-"}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateDaftarInformasi.findUnique.data?.deskripsi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Box py="md">
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="xl">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Informasi Publik
</Text>
<Paper bg="#f8f9fa" p="md" radius="md" shadow="xs">
<Stack gap="lg">
<Box>
<Text fz="lg" fw="bold" mb={4}>Jenis Informasi</Text>
<Text fz="md" c="dimmed">{data.jenisInformasi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold" mb={4}>Tanggal Publikasi</Text>
<Text fz="md" c="dimmed">
{data.tanggal ? new Date(data.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
<Box
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
className="prose max-w-none"
/>
</Box>
<Group gap="sm" mt="md">
<Tooltip label="Edit Informasi" withArrow position="top">
<Button
onClick={() => {
if (stateDaftarInformasi.findUnique.data) {
setSelectedId(stateDaftarInformasi.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDaftarInformasi.delete.loading || !stateDaftarInformasi.findUnique.data}
color={"red"}
variant="light"
color="green"
leftSection={<IconEdit size={18} />}
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik-desa-darmasaba/${data.id}/edit`)}
disabled={!data}
>
<IconX size={20} />
Edit
</Button>
</Tooltip>
<Tooltip label="Hapus Informasi" withArrow position="top">
<Button
variant="light"
color="red"
leftSection={<IconTrash size={18} />}
onClick={() => {
if (stateDaftarInformasi.findUnique.data) {
router.push(`/admin/ppid/daftar-informasi-publik-desa-darmasaba/${stateDaftarInformasi.findUnique.data.id}/edit`);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={!stateDaftarInformasi.findUnique.data}
color={"green"}
disabled={stateDaftarInformasi.delete.loading || !data}
loading={stateDaftarInformasi.delete.loading}
>
<IconEdit size={20} />
Hapus
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
text='Apakah Anda yakin ingin menghapus informasi ini? Tindakan ini tidak dapat dibatalkan.'
/>
</Box>
);

View File

@@ -1,70 +1,124 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import daftarInformasiPublik from '../../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
export default function CreateBerita() {
const daftarInformasi = useProxy(daftarInformasiPublik)
const router = useRouter()
export default function CreateDaftarInformasi() {
const daftarInformasi = useProxy(daftarInformasiPublik);
const router = useRouter();
const resetForm = () => {
// Reset state di valtio
daftarInformasi.create.form = {
jenisInformasi: "",
deskripsi: "",
tanggal: "",
};
// Reset state lokal
};
const handleSubmit = async () => {
// Submit data berita
await daftarInformasi.create.create();
if (!daftarInformasi.create.form.jenisInformasi) {
return alert('Mohon isi jenis informasi');
}
if (!daftarInformasi.create.form.deskripsi) {
return alert('Mohon isi deskripsi');
}
if (!daftarInformasi.create.form.tanggal) {
return alert('Mohon pilih tanggal publikasi');
}
// Reset form setelah submit
resetForm();
router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba")
try {
await daftarInformasi.create.create();
resetForm();
router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba");
} catch (error) {
console.error('Error creating informasi publik:', error);
alert('Terjadi kesalahan saat menyimpan data');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Daftar Informasi Publik Desa Darmasaba</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => {
daftarInformasi.create.form.jenisInformasi = val.target.value
}}
/>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Informasi Publik
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xl">
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={daftarInformasi.create.form.deskripsi}
onChange={(htmlContent) => {
daftarInformasi.create.form.deskripsi = htmlContent;
<Text fw="bold" fz="sm" mb={6}>
Jenis Informasi
</Text>
<TextInput
placeholder="Contoh: Profil Desa, Laporan Keuangan, dll"
value={daftarInformasi.create.form.jenisInformasi}
onChange={(e) => {
daftarInformasi.create.form.jenisInformasi = e.target.value;
}}
required
/>
</Box>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
type="date"
placeholder="Contoh: 2022-01-01"
value={daftarInformasi.create.form.tanggal}
onChange={(e) => (daftarInformasi.create.form.tanggal = e.currentTarget.value)}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<Box style={{ border: '1px solid #dee2e6', borderRadius: '0.25rem' }}>
<CreateEditor
value={daftarInformasi.create.form.deskripsi}
onChange={(htmlContent) => {
daftarInformasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Tanggal Publikasi
</Text>
<TextInput
type="date"
value={daftarInformasi.create.form.tanggal}
onChange={(e) => {
daftarInformasi.create.form.tanggal = e.target.value;
}}
required
/>
</Box>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={daftarInformasi.create.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,13 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useShallowEffect, useViewportSize } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
function DaftarInformasiPublik() {
@@ -16,7 +15,7 @@ function DaftarInformasiPublik() {
<Box>
<HeaderSearch
title='Daftar Informasi Publik Desa Darmasaba'
placeholder='pencarian'
placeholder='Cari jenis informasi atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,115 +30,118 @@ function ListDaftarInformasi({ search }: { search: string }) {
const router = useRouter()
const { data, page, totalPages, loading, load } = listData.findMany
const { width } = useViewportSize()
const isMobile = width < 768
useEffect(() => {
load(page, 10)
}, [page])
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.jenisInformasi.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={790} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Daftar Informasi Publik Desa Darmasaba'
href='/admin/ppid/daftar-informasi-publik-desa-darmasaba/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data daftar informasi publik yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 870, md: 790 }}>
<Stack>
<JudulList
title='List Daftar Informasi Publik Desa Darmasaba'
href='/admin/ppid/daftar-informasi-publik-desa-darmasaba/create'
/>
<Box style={{ overflowX: "auto" }}>
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Informasi Publik</Title>
<Tooltip label="Tambah Informasi Publik" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/daftar-informasi-publik-desa-darmasaba/create')}
>
{isMobile ? 'Tambah' : 'Tambah Baru'}
</Button>
</Tooltip>
</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>
</Stack>
) : (
<Box style={{ overflowX: 'auto' }}>
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
bg={colors['white-1']}
withColumnBorders
striped
stickyHeader
style={{ minWidth: '700px' }}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Jenis Informasi</TableTh>
<TableTh style={{ width: '50%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Jenis Informasi</TableTh>
<TableTh style={{ width: '60%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '10%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>
<Text mt={10} fz={"md"}>{index + 1}</Text>
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
</TableTd>
<TableTd style={{ wordWrap: 'break-word' }}>
<Box w={200}>
<Text mt={10} fz={"md"} truncate={"end"} lineClamp={1}>{item.jenisInformasi}</Text>
</Box>
<TableTd>
<Tooltip label={item.jenisInformasi} position="top-start" openDelay={500}>
<Text fw={500} lineClamp={1}>{item.jenisInformasi}</Text>
</Tooltip>
</TableTd>
<TableTd style={{ wordWrap: 'break-word' }}>
<Box w={200}>
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }}></Text>
</Box>
<TableTd>
<Tooltip
label={item.deskripsi?.replace(/<[^>]*>?/gm, '')}
position="top-start"
openDelay={500}
multiline
maw={400}
>
<Text lineClamp={1} fz="sm" c="dimmed">
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80) + '...'}
</Text>
</Tooltip>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
>
<IconDeviceImacCog size={20} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
)}
</Paper>
<Center>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -1,6 +1,6 @@
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
@@ -41,45 +41,70 @@ function EditDasarHukum() {
router.push('/admin/ppid/dasar-hukum')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Dasar Hukum PPID</Title>
<Text fw={"bold"}>Judul</Text>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Dasar Hukum PPID
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xl">
<Box>
<Text fw="bold" fz="sm" mb={6}>
Judul
</Text>
<Box style={{ border: '1px solid #dee2e6', borderRadius: '0.25rem' }}>
<PPIDTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
</Box>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Konten
</Text>
<Box style={{ border: '1px solid #dee2e6', borderRadius: '0.25rem' }}>
<PPIDTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={dasarHukumState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
</Box>
<Group justify="flex-end" mt="md">
<Button
onClick={submit}
loading={dasarHukumState.update.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,6 +1,6 @@
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -14,38 +14,78 @@ function Page() {
listDasarHukum.findById.load('1')
}, [])
if (listDasarHukum.findById.loading) {
return (
<Center py={40}>
<Skeleton radius="md" height={800} width="100%" />
</Center>
);
}
if (!listDasarHukum.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
<Center py={60}>
<Stack align="center" gap="sm">
<Text fw={500} c="dimmed">Belum ada data dasar hukum PPID</Text>
</Stack>
</Center>
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Dasar Hukum PPID</Title>
<Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}>
<IconEdit size={16} />
</Button>
<Tooltip label="Edit Dasar Hukum" withArrow>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text ta={"center"} fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }} />
</Box>
</Paper>
</Stack>
</Box>
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 'sm', md: 100 }}>
<Grid>
<GridCol span={12}>
<Center>
<Image 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"
c={colors['blue-button']}
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
/>
</GridCol>
</Grid>
<Divider my="xl" color={colors['blue-button']} />
<Box
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
style={{
fontSize: '1.1rem',
lineHeight: 1.7,
textAlign: 'justify'
}}
/>
</Box>
</Paper>
</Stack>
</Paper>
)

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
'use client';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconChartBar, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -11,15 +11,18 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
const tabs = [
{
label: "Indeks Kepuasan Masyarakat",
description: "Lihat dan kelola indeks kepuasan masyarakat",
value: "indekskepuasannamasyarakat",
href: "/admin/ppid/ikm-desa-darmasaba/indeks-kepuasan-masyarakat"
href: "/admin/ppid/ikm-desa-darmasaba/indeks-kepuasan-masyarakat",
icon: <IconChartBar size={18} stroke={1.8} />
},
{
label: "Responden",
description: "Kelola dan tinjau data responden",
value: "responden",
href: "/admin/ppid/ikm-desa-darmasaba/responden"
href: "/admin/ppid/ikm-desa-darmasaba/responden",
icon: <IconUsers size={18} stroke={1.8} />
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
@@ -40,16 +43,48 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
}, [pathname])
return (
<Stack>
<Title order={3}>IKM Desa Darmasaba</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<Stack gap="lg">
<Title order={2} style={{ fontWeight: 700, color: "#1a1a1a" }}>
IKM Desa Darmasaba
</Title>
<Tabs
radius="xl"
color="blue"
variant="pills"
value={activeTab}
onChange={handleTabChange}
>
<TabsList
p="sm"
style={{
background: "#F3F4FB",
borderRadius: "1rem",
}}
>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Tooltip
key={i}
label={e.description}
withArrow
position="bottom"
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={e.value}
leftSection={e.icon}
style={{
fontWeight: 500,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{e.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
<TabsPanel key={i} value={e.value} mt="md">
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>

View File

@@ -116,28 +116,27 @@ function Page() {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
<Stack py="xl">
<Skeleton height={750} radius="lg" />
</Stack>
);
}
if (data.length === 0) {
return (
<Stack py={10}>
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan
<Stack py="xl">
<Text c="dimmed" ta="center" size="lg">
Belum ada data yang tersedia
</Text>
</Stack>
);
}
return (
<Stack gap="xs">
{/* Bar Chart - Data per Tanggal */}
<Paper bg={colors['white-1']} p="md" radius="md" mb="md">
<Title order={4} mb="md" ta="center">Jumlah Responden per Bulan</Title>
<Box h={300}>
<Stack gap="lg">
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Title order={3} mb="md" ta="center">Tren Jumlah Responden</Title>
<Box h={320}>
<BarChart
h={300}
data={barChartData}
@@ -152,14 +151,13 @@ function Page() {
</Box>
</Paper>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
<Title order={4} ta="center">Distribusi Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
Tidak ada data
</Text>
) : (
<Box>
@@ -168,15 +166,15 @@ function Page() {
withLabels
withTooltip
labelsType="percent"
size={250}
size={220}
data={donutDataJenisKelamin}
/>
</Center>
<Stack gap="sm" mt="md">
<Stack gap="xs" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Flex key={entry.name} gap="sm" align="center">
<Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
<Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
@@ -185,13 +183,12 @@ function Page() {
</Stack>
</Paper>
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Stack>
<Title order={4}>Pilihan</Title>
<Title order={4} ta="center">Distribusi Penilaian</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
Tidak ada data
</Text>
) : (
<Box>
@@ -200,15 +197,15 @@ function Page() {
withLabels
withTooltip
labelsType="percent"
size={250}
size={220}
data={donutDataRating}
/>
</Center>
<Stack gap="sm" mt="md">
<Stack gap="xs" mt="md">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Flex key={entry.name} gap="sm" align="center">
<Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
<Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
@@ -217,13 +214,12 @@ function Page() {
</Stack>
</Paper>
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Stack>
<Title order={4}>Umur</Title>
<Title order={4} ta="center">Distribusi Kelompok Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
Tidak ada data
</Text>
) : (
<Box>
@@ -232,15 +228,15 @@ function Page() {
withLabels
withTooltip
labelsType="percent"
size={250}
size={220}
data={donutDataKelompokUmur}
/>
</Center>
<Stack gap="sm" mt="md">
<Stack gap="xs" mt="md">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Flex key={entry.name} gap="sm" align="center">
<Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
<Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>

View File

@@ -4,8 +4,8 @@ import React, { useEffect, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Text, Select } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { Box, Button, Group, Paper, Stack, Title, TextInput, Text, Select, Tooltip } from '@mantine/core';
import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import { toast } from 'react-toastify';
@@ -78,19 +78,35 @@ function EditResponden() {
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Responden</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Responden
</Title>
</Group>
<Paper
withBorder
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<TextInput
label="Nama"
label={
<Text fw="bold" fz="sm" mb={4}>
Nama Responden
</Text>
}
type='text'
placeholder="masukkan nama"
placeholder="Masukkan nama responden"
value={formData.name}
onChange={(val) => {
setFormData({
@@ -98,9 +114,15 @@ function EditResponden() {
name: val.currentTarget.value
})
}}
radius="md"
required
/>
<TextInput
label="Tanggal"
label={
<Text fw="bold" fz="sm" mb={4}>
Tanggal
</Text>
}
type="date"
placeholder='Pilih tanggal'
value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
@@ -111,32 +133,43 @@ function EditResponden() {
tanggal: selectedDate,
});
}}
radius="md"
required
/>
<Select
key={"jenisKelamin"}
label={<Text fw="bold" fz="sm">Jenis Kelamin</Text>}
key="jenisKelamin"
label={
<Text fw="bold" fz="sm" mb={4}>
Jenis Kelamin
</Text>
}
placeholder="Pilih jenis kelamin"
value={formData.jenisKelaminId}
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val || "" })}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null/undefined
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} // ✅ disable saat loading
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
clearable
searchable
required
radius="md"
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
/>
<Select
key={"rating"}
key="rating"
value={formData.ratingId}
onChange={(val) => setFormData({ ...formData, ratingId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Rating</Text>}
label={
<Text fw="bold" fz="sm" mb={4}>
Rating
</Text>
}
placeholder='Pilih rating'
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
@@ -150,6 +183,7 @@ function EditResponden() {
clearable
searchable
required
radius="md"
error={!formData.ratingId ? "Pilih rating" : undefined}
/>
@@ -173,14 +207,24 @@ function EditResponden() {
required
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="red"
onClick={() => router.back()}
>
Batal
</Button>
<Button
leftSection={<IconDeviceFloppy size={20} />}
onClick={handleSubmit}
loading={state.update.loading}
color={colors['blue-button']}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -3,10 +3,10 @@
import { ModalKonfirmasiHapus } from "@/app/admin/(dashboard)/_com/modalKonfirmasiHapus"
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"
import colors from "@/con/colors"
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from "@mantine/core"
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from "@mantine/core"
import { useShallowEffect } from "@mantine/hooks"
import { IconArrowBack, IconEdit, IconX } from "@tabler/icons-react"
import { useRouter, useParams } from "next/navigation"
import { IconArrowBack, IconEdit, IconTrash } from "@tabler/icons-react"
import { useParams, useRouter } from "next/navigation"
import { useState } from "react"
import { useProxy } from "valtio/utils"
@@ -38,67 +38,89 @@ export default function DetailResponden() {
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Responden</Text>
<Box py={10}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Responden
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Responden</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.name}</Text>
<Text fz="lg" fw="bold">Nama Responden</Text>
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal</Text>
<Text fz={"lg"}>{
<Text fz="lg" fw="bold">Tanggal</Text>
<Text fz="md" c="dimmed">{
stateDetail.findUnique.data?.tanggal
? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
: '-'
}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jenis Kelamin</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.jenisKelamin?.name}</Text>
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.jenisKelamin?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.rating?.name}</Text>
<Text fz="lg" fw="bold">Rating</Text>
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.rating?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kelompok Umur</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.kelompokUmur?.name}</Text>
<Text fz="lg" fw="bold">Kelompok Umur</Text>
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.kelompokUmur?.name || '-'}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`);
}
}}
disabled={!stateDetail.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Responden" withArrow position="top">
<Button
color="red"
variant="light"
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
leftSection={<IconTrash size={20} />}
>
Hapus
</Button>
</Tooltip>
<Tooltip label="Edit Responden" withArrow position="top">
<Button
color="green"
variant="light"
onClick={() => {
if (stateDetail.findUnique.data) {
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`);
}
}}
disabled={!stateDetail.findUnique.data}
leftSection={<IconEdit size={20} />}
>
Edit
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>

View File

@@ -1,5 +1,4 @@
'use client';
import colors from '@/con/colors';
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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
@@ -14,9 +13,9 @@ function Responden() {
return (
<Box>
<HeaderSearch
title='Responden'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
title="Data Responden"
placeholder="Cari nama responden..."
searchIcon={<IconSearch size={18} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
@@ -49,93 +48,99 @@ function ListResponden({ search }: ListRespondenProps) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
<Stack py="xl">
<Skeleton height={750} radius="lg" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md">
<Stack>
<Title>Responden</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Title order={3}>Data Responden</Title>
<Table striped withTableBorder 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">
Belum ada data responden yang tersedia
</Text>
</Stack>
</Paper>
);
}
return (
<Box>
<Stack gap="xs">
<Paper bg={colors['white-1']} p="md" h={{ base: 730, md: 650 }}>
<Title mb={10} order={3}>List Responden</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Title order={3}>Data Responden</Title>
<Table striped withTableBorder 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 ? (
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
<TableTd colSpan={5}>
<Text c="dimmed" ta="center" py="md">
Tidak ada data yang cocok dengan pencarian
</Text>
</TableTd>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data responden</Text>
) : (
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
variant="light"
size="xs"
onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}
>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.name}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID')
: '-'}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.jenisKelamin.name}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
))
)}
</TableTbody>
</Table>
{filteredData.length > 0 && (
<Center>
<Pagination
value={page}
total={totalPages}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
size="md"
radius="md"
mt="md"
/>
</Center>
)}
</Stack>
</Box>
</Paper>
);
}

View File

@@ -1,58 +1,104 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik';
import { useProxy } from 'valtio/utils'
import { useShallowEffect } from '@mantine/hooks'
import { Box, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title, Text, Group, Tooltip, Badge } from '@mantine/core'
import { IconInfoCircle, IconUser, IconMail, IconPhone, IconId } from '@tabler/icons-react'
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
import colors from '@/con/colors'
function Page() {
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
useShallowEffect(() => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.load()
}, [])
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data)
return <Stack pos={"relative"} bg={colors.Bg}>
<Skeleton radius={5} h={200} />
</Stack>
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data) {
return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Skeleton radius="md" h={200} w="100%" />
</Stack>
)
}
const data = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data
return (
<Box py={5}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Permohonan Informasi Publik</Title>
<Box>
<Table striped withRowBorders withColumnBorders withTableBorder>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>NIK</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Cara Memperoleh Informasi</TableTh>
<TableTh>Cara Memperoleh Salinan Informasi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data?.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>{item.nik}</TableTd>
<TableTd>{item.notelp}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>{item.jenisInformasiDiminta?.name}</TableTd>
<TableTd>{item.caraMemperolehInformasi?.name}</TableTd>
<TableTd>{item.caraMemperolehSalinanInformasi?.name}</TableTd>
<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>
<Tooltip label="Data permohonan informasi sesuai dengan pengajuan masyarakat" position="bottom">
<IconInfoCircle size={20} stroke={1.5} />
</Tooltip>
</Group>
{data.length === 0 ? (
<Stack align="center" py="xl">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">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}><IconMail size={16} /> Email</Group></TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Cara Akses Informasi</TableTh>
<TableTh>Salinan Informasi</TableTh>
</TableTr>
))}
</TableTbody>
</Table> </Box>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Tooltip label={item.name}>
<Text lineClamp={1} fw={500}>{item.name}</Text>
</Tooltip>
</TableTd>
<TableTd>{item.nik}</TableTd>
<TableTd>{item.notelp}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>
<Badge variant="light" radius="sm" color="blue">
{item.jenisInformasiDiminta?.name || '-'}
</Badge>
</TableTd>
<TableTd>
<Tooltip label={item.caraMemperolehInformasi?.name}>
<Text lineClamp={1} size="sm">{item.caraMemperolehInformasi?.name || '-'}</Text>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label={item.caraMemperolehSalinanInformasi?.name}>
<Text lineClamp={1} size="sm">{item.caraMemperolehSalinanInformasi?.name || '-'}</Text>
</Tooltip>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
)}
</Stack>
</Paper>
</Box>
);
)
}
export default Page;
export default Page

View File

@@ -1,9 +1,10 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi';
import colors from '@/con/colors'
import { Box, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'
import { useShallowEffect } from '@mantine/hooks'
import { IconInfoCircle, IconMail, IconMessage, IconPhone, IconUser } from '@tabler/icons-react'
import { useProxy } from 'valtio/utils'
import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi'
function Page() {
const listState = useProxy(statePermohonanKeberatan)
@@ -11,38 +12,90 @@ function Page() {
listState.findMany.load()
}, [])
if (!listState.findMany.data)
return <Stack pos={"relative"} bg={colors.Bg}>
<Skeleton radius={5} h={200} />
</Stack>
if (!listState.findMany.data) {
return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Skeleton radius="md" h={200} w="100%" />
</Stack>
)
}
const data = listState.findMany.data
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Permohonan Keberatan Informasi Publik</Title>
<Table striped withRowBorders withColumnBorders withTableBorder>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Email</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Alasan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{listState.findMany.data?.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>{item.notelp}</TableTd>
<TableTd dangerouslySetInnerHTML={{ __html: item.alasan }} />
</TableTr>
))}
</TableTbody>
</Table>
<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>
<Tooltip label="Data permohonan keberatan atas informasi yang diajukan masyarakat" position="bottom">
<IconInfoCircle size={20} stroke={1.5} />
</Tooltip>
</Group>
{data.length === 0 ? (
<Stack align="center" py="xl">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">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}><IconMessage size={16} /> Alasan Keberatan</Group></TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Tooltip label={item.name}>
<Text lineClamp={1} fw={500}>{item.name}</Text>
</Tooltip>
</TableTd>
<TableTd>
<Text size="sm">{item.email || '-'}</Text>
</TableTd>
<TableTd>
<Text>{item.notelp || '-'}</Text>
</TableTd>
<TableTd>
<Tooltip label={item.alasan?.replace(/<[^>]*>?/gm, '')}>
<div
style={{
maxWidth: '300px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
dangerouslySetInnerHTML={{
__html: item.alasan ?
item.alasan.replace(/<[^>]*>?/gm, '').substring(0, 50) + '...' :
'-'
}}
/>
</Tooltip>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
)}
</Stack>
</Paper>
</Box>

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID';
@@ -146,129 +146,139 @@ function EditProfilePPID() {
}
return (
<Box>
<Stack gap="xs">
<Box>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box p="md">
<Stack gap="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Profil PPID
</Title>
</Group>
<Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}>
<Stack gap="xs">
<Title order={3}>Edit Profile PPID</Title>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Title order={3} c={colors['blue-button']}>Edit Profile PPID</Title>
{/* Nama Field */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.editForm.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.editForm.form.name && "Nama wajib diisi"}
/>
{/* Nama Field */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.editForm.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.editForm.form.name && "Nama wajib diisi"}
/>
{/* File Upload */}
{/* File Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
<div>
<Text size="xl" inline>
Tarik gambar ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
</Box>
{/* Rich Components */}
<Biodata
value={allState.editForm.form.biodata}
onChange={(val) => handleFieldChange('biodata', val)}
/>
{/* Rich Components */}
<Biodata
value={allState.editForm.form.biodata}
onChange={(val) => handleFieldChange('biodata', val)}
/>
<RiwayatKarir
value={allState.editForm.form.riwayat}
onChange={(val) => handleFieldChange('riwayat', val)}
/>
<RiwayatKarir
value={allState.editForm.form.riwayat}
onChange={(val) => handleFieldChange('riwayat', val)}
/>
<PengalamanOrganisasi
value={allState.editForm.form.pengalaman}
onChange={(val) => handleFieldChange('pengalaman', val)}
/>
<PengalamanOrganisasi
value={allState.editForm.form.pengalaman}
onChange={(val) => handleFieldChange('pengalaman', val)}
/>
<ProgramKerjaUnggulan
value={allState.editForm.form.unggulan}
onChange={(val) => handleFieldChange('unggulan', val)}
/>
<ProgramKerjaUnggulan
value={allState.editForm.form.unggulan}
onChange={(val) => handleFieldChange('unggulan', val)}
/>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || allState.editForm.loading}
disabled={!allState.editForm.form.name}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || allState.editForm.loading}
disabled={!allState.editForm.form.name}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.editForm.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.editForm.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -16,9 +16,11 @@ function Page() {
}, [])
if (!allList.profile.data) {
return <Stack>
<Skeleton radius={10} h={800} />
</Stack>
return (
<Stack align="center" justify="center" py="xl">
<Skeleton radius="md" height={800} />
</Stack>
);
}
const dataArray = Array.isArray(allList.profile.data)
@@ -26,89 +28,96 @@ function Page() {
: [allList.profile.data];
return (
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Grid>
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Profile PPID</Title>
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/ppid/profile-ppid/${allList.profile.data?.id}`)}>
<IconEdit size={16} />
</Button>
<Tooltip label="Edit Profil PPID" withArrow>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/ppid/profile-ppid/${allList.profile.data?.id}`)}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
{dataArray.map((item) => (
<Box key={item.id} >
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: "md", md: 100 }}>
<Grid>
<GridCol span={{ base: 12, md: 12 }}>
<Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
</GridCol>
<GridCol span={{ base: 12, md: 12 }}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
</GridCol>
</Grid>
</Box>
<Divider my={"md"} color={colors['blue-button']} />
{/* biodata perbekel */}
<Box px={{ base: 0, md: 50 }} pb={30}>
<Box pb={20} px={{ base: 0, md: 50 }}>
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}>
<Center>
<Image
pt={{ base: 0, md: 90 }}
src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
alt='Foto Profil PPID'
onError={(e) => {
e.currentTarget.src = "/perbekel.png";
}}
/>
</Center>
<Paper
bg={colors['blue-button']}
py={20}
className="glass3"
px={{ base: 10, md: 10 }}
>
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
<Paper key={item.id} p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: "sm", md: 100 }}>
<Grid>
<GridCol span={12}>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
</Center>
</GridCol>
<GridCol span={12}>
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
</Text>
</GridCol>
</Grid>
</Box>
<Divider my="md" color={colors['blue-button']} />
<Box px={{ base: 0, md: 50 }} pb="xl">
<Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image
pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
alt="Foto Profil PPID"
radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
/>
</Center>
<Paper
bg={colors['blue-button']}
py="md"
px="sm"
radius="md"
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" }}>
{item.name}
</Text>
</Paper>
</Box>
<Box pt={10}>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: item.biodata }} />
</Box>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Riwayat Karir</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} />
</Box>
</Box>
<Box pb={30}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman Organisasi</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: item.pengalaman }} />
</Box>
</Box>
<Box pb={20}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: item.unggulan }} />
</Box>
</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']} 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']} dangerouslySetInnerHTML={{ __html: item.riwayat }} />
</Box>
<Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Pengalaman Organisasi</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: item.pengalaman }} />
</Box>
</Box>
</Paper>
</Box>
<Box mt="xl" mb="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: item.unggulan }} />
</Box>
</Box>
</Box>
</Paper>
))}
</Stack>
</Paper>

View File

@@ -1,65 +1,108 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ppid/struktur-ppid/pegawai"
href: "/admin/ppid/struktur-ppid/pegawai",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola daftar pegawai PPID"
},
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ppid/struktur-ppid/posisi-organisasi"
href: "/admin/ppid/struktur-ppid/posisi-organisasi",
icon: <IconHierarchy2 size={18} stroke={1.8} />,
tooltip: "Kelola posisi dalam struktur organisasi"
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ppid/struktur-ppid/struktur-organisasi"
href: "/admin/ppid/struktur-ppid/struktur-organisasi",
icon: <IconBuildingCommunity size={18} stroke={1.8} />,
tooltip: "Kelola struktur organisasi PPID"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href)
router.push(tab.href);
}
setActiveTab(value)
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value)
setActiveTab(match.value);
}
}, [pathname])
}, [pathname]);
return (
<Stack>
<Title order={3}>Struktur PPID Desa Darmasaba</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Struktur PPID
</Title>
<Tabs
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
{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}
</Stack>
);
}

View File

@@ -13,7 +13,8 @@ import {
Stack,
Text,
TextInput,
Title
Title,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -134,142 +135,190 @@ export default function EditPegawaiPPID() {
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Data Pegawai PPID
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Data Pegawai PPID</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={formData.namaLengkap}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setFile(file);
setPreviewImage(URL.createObjectURL(file)); // Buat preview
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>
Nama Lengkap
</Text>
<TextInput
placeholder="Masukkan nama lengkap"
value={formData.namaLengkap}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
required
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gelar Akademik
</Text>
<TextInput
placeholder="Contoh: S.Kom"
value={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Foto Profil
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
/>
)}
</Box>
</Box>
)}
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Tanggal Masuk
</Text>
<TextInput
type="date"
placeholder="Contoh: 2022-01-01"
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Email
</Text>
<TextInput
type="email"
placeholder="contoh@email.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Telepon
</Text>
<TextInput
placeholder="08123456789"
value={formData.telepon}
onChange={(e) => setFormData({ ...formData, telepon: e.target.value })}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Alamat
</Text>
<TextInput
placeholder="Jl. Contoh No. 123"
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Posisi
</Text>
<Select
placeholder="Pilih posisi"
data={stateStrukturPPID.posisiOrganisasi.findMany.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
value={formData.posisiId}
onChange={(value) => value && setFormData({ ...formData, posisiId: value })}
searchable
clearable
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Status Pegawai
</Text>
<Select
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)}
onChange={(val) => {
setFormData({ ...formData, isActive: val === 'true' });
}}
clearable
/>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
value={formData.email}
onChange={(e) => (formData.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
value={formData.telepon}
onChange={(e) => (formData.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
value={formData.alamat}
onChange={(e) => (formData.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={
stateStrukturPPID.posisiOrganisasi.findMany.data?.map((p) => ({
value: p.id, // harus string
label: p.nama,
})) || []
}
value={formData.posisiId}
onChange={(value) => {
if (value !== null) {
setFormData({ ...formData, posisiId: value }); // value harus string
}
}}
/>
<Select
label="Status Pegawai"
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)} // 'true' atau 'false'
onChange={(val) => {
setFormData({ ...formData, isActive: val === 'true' });
}}
/>
<Group>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
color="blue"
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
Simpan Perubahan
</Button>
</Group>
</Stack>
</Paper>
</Box >
</Box>
);
}

View File

@@ -2,41 +2,43 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } 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';
function DetailPegawai() {
const statePegawai = useProxy(stateStrukturPPID.pegawai)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const statePegawai = useProxy(stateStrukturPPID.pegawai);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePegawai.findUnique.load(params?.id as string)
}, [])
statePegawai.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ppid/struktur-ppid/pegawai")
statePegawai.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ppid/struktur-ppid/pegawai");
}
}
};
if (!statePegawai.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = statePegawai.findUnique.data;
return (
<Box>
<Box mb={10}>
@@ -44,91 +46,111 @@ function DetailPegawai() {
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pegawai PPID</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pegawai PPID
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="md">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Lengkap</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gelar Akademik</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.gelarAkademik}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Image</Text>
{statePegawai.findUnique.data?.image?.link ? (
<Image src={statePegawai.findUnique.data?.image?.link} alt='' />
) : (
<Text fz={"md"} c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Masuk</Text>
<Text fz={"lg"}>
{statePegawai.findUnique.data?.tanggalMasuk
? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString()
: "-"}
<Text fz="lg" fw="bold">Nama Lengkap</Text>
<Text fz="md" c="dimmed">
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Email</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Telepon</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.telepon}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Posisi</Text>
<Stack gap={4}>
{statePegawai.findUnique.data?.posisi ? (
<Text fz={"lg"}>
{statePegawai.findUnique.data.posisi.nama}
</Text>
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada posisi
</Text>
)}
</Stack>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Aktif</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
setSelectedId(statePegawai.findUnique.data.id);
setModalHapus(true);
}
<Text fz="lg" fw="bold">Posisi</Text>
<Text fz="md" c="dimmed">{data.posisi?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Email</Text>
<Text fz="md" c="dimmed">{data.email || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Telepon</Text>
<Text fz="md" c="dimmed">{data.telepon || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tanggal Masuk</Text>
<Text fz="md" c="dimmed">
{data.tanggalMasuk ? new Date(data.tanggalMasuk).toLocaleDateString() : '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Status</Text>
<Text fz="md" c={data.isActive ? 'green' : 'red'}>
{data.isActive ? 'Aktif' : 'Tidak Aktif'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Foto Profil</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.namaLengkap || 'Foto Profil'}
w={200}
h={200}
radius="md"
fit="cover"
mt="sm"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
disabled={!statePegawai.findUnique.data}
color="red">
<IconX size={20} />
/>
) : (
<Text fz="sm" c="dimmed" mt="sm">Tidak ada foto profil</Text>
)}
</Box>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Pegawai" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id || null);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Pegawai" withArrow position="top">
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ppid/struktur-ppid/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.findUnique.data}
color="green">
color="green"
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
@@ -139,7 +161,7 @@ function DetailPegawai() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus pegawai ini?"
text="Apakah Anda yakin ingin menghapus data pegawai ini?"
/>
</Box>
);

View File

@@ -3,7 +3,7 @@
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -30,7 +30,7 @@ function CreatePegawaiPPID() {
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
isActive: true,
};
};
@@ -53,14 +53,14 @@ function CreatePegawaiPPID() {
// Set status aktif secara otomatis
stateOrganisasi.create.form.isActive = true;
// Simpan ID gambar ke form
stateOrganisasi.create.form.imageId = uploaded.id;
// Submit form
await stateOrganisasi.create.submit();
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
@@ -72,125 +72,201 @@ function CreatePegawaiPPID() {
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pegawai</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pegawai PPID
</Title>
</Group>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" fz="sm" mb={6}>
Nama Lengkap
</Text>
<TextInput
placeholder="Masukkan nama lengkap"
value={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
required
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gelar Akademik
</Text>
<TextInput
placeholder="Contoh: S.Kom"
value={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Foto Profil
</Text>
<Dropzone
onDrop={(files) => {
const file = files[0];
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
styles={{
root: {
border: '2px dashed #ced4da',
borderRadius: '8px',
padding: '20px',
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: '#228be6',
},
},
}}
>
<Group justify="center" gap="xl" mih={160} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="md" inline>
Seret gambar ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Format yang didukung: JPG, PNG, WebP. Maksimal 5MB
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Preview Gambar
</Text>
<Image
src={previewImage.preview}
alt="Preview"
width={280}
height={180}
width={200}
height={200}
fit="cover"
radius="sm"
mt="md"
radius="md"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
/>
)}
</Box>
</Box>
)}
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Tanggal Masuk
</Text>
<TextInput
type="date"
placeholder="Contoh: 2022-01-01"
value={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Email
</Text>
<TextInput
type="email"
placeholder="Contoh: email@example.com"
value={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Nomor Telepon
</Text>
<TextInput
placeholder="Contoh: 08123456789"
value={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Alamat
</Text>
<TextInput
placeholder="Contoh: Jl. Contoh No. 1"
value={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Posisi
</Text>
<Select
placeholder="Pilih posisi"
data={stateStrukturPPID.posisiOrganisasi.findMany.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
value={stateOrganisasi.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.create.form.posisiId = value;
}}
searchable
clearable
/>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
value={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
value={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
value={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={stateStrukturPPID.posisiOrganisasi.findMany.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
value={stateOrganisasi.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.create.form.posisiId = value;
}}
searchable
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,13 +1,12 @@
/* 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, ThemeIcon } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
function PegawaiPPID() {
@@ -16,7 +15,7 @@ function PegawaiPPID() {
<Box>
<HeaderSearch
title='Pegawai PPID'
placeholder='pencarian'
placeholder='Cari nama pegawai atau posisi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -56,46 +55,51 @@ function ListPegawaiPPID({ search }: { search: string }) {
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pegawai'
href='/admin/ppid/struktur-ppid/pegawai/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
<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>
<Tooltip label="Tambah Pegawai Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ppid/struktur-ppid/pegawai/create'
/>
<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>
<Tooltip label="Tambah Pegawai Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
<TableTh style={{ width: '10%' }}>Status</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -112,10 +116,20 @@ function ListPegawaiPPID({ search }: { search: string }) {
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
@@ -137,8 +151,13 @@ function ListPegawaiPPID({ search }: { search: string }) {
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
size="sm"
>
<IconDeviceImacCog size={20} />
</Button>
</TableTd>
</TableTr>
@@ -146,19 +165,20 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -1,9 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -77,24 +77,39 @@ function EditPosisiOrganisasiPPID() {
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Posisi Organisasi PPID
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posisi Organisasi PPID</Title>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Posisi Organisasi"
placeholder="Masukkan nama posisi organisasi"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
placeholder='Masukkan nama posisi organisasi'
required
/>
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -102,14 +117,34 @@ function EditPosisiOrganisasiPPID() {
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
value={formData.hierarki}
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Hierarki</Text>}
placeholder='Masukkan hierarki'
onChange={(e) => {
const value = parseInt(e.target.value, 10);
setFormData({ ...formData, hierarki: isNaN(value) ? 0 : value });
}}
required
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,82 +1,127 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePosisiOrganisasiPPID() {
const router = useRouter();
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi)
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const resetForm = () => {
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0, // Initialize as 0 to allow any number input
};
const router = useRouter();
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi);
useEffect(() => {
stateOrganisasi.findMany.load();
// Initialize form with default values
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
const handleSubmit = async () => {
await stateOrganisasi.create.submit();
resetForm();
router.push("/admin/ppid/struktur-ppid/posisi-organisasi")
return () => {
// Clean up form on unmount
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
};
}, []);
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Posisi Organisasi PPID</Title>
<TextInput
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
placeholder="Contoh: 1"
value={stateOrganisasi.create.form.hierarki}
onChange={(e) => {
const value = parseInt(e.currentTarget.value, 10);
if (!isNaN(value)) {
stateOrganisasi.create.form.hierarki = value;
}
}}
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
const handleSubmit = async () => {
try {
if (!stateOrganisasi.create.form.nama.trim()) {
return toast.error('Nama posisi tidak boleh kosong');
}
await stateOrganisasi.create.submit();
toast.success('Posisi organisasi berhasil ditambahkan');
router.push('/admin/ppid/struktur-ppid/posisi-organisasi');
} catch (error) {
toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Posisi Organisasi PPID
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
value={stateOrganisasi.create.form.hierarki || ''}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
}}
required
/>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.create.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePosisiOrganisasiPPID;

View File

@@ -1,13 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
@@ -17,7 +16,7 @@ function PosisiOrganisasiPPID() {
<Box>
<HeaderSearch
title='Posisi Organisasi PPID'
placeholder='pencarian'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -41,7 +40,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
load,
} = stateOrganisasi.findMany;
useEffect(() => {
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
@@ -58,64 +57,88 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Posisi Organisasi PPID'
href='/admin/ppid/struktur-ppid/posisi-organisasi/create'
/>
<Box style={{ overflowY: 'auto', maxHeight: '400px' }}>
<Table striped withTableBorder withRowBorders>
<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>
<Tooltip label="Tambah Posisi Organisasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Posisi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Hierarki</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
<TableTh style={{ width: '25%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Hierarki</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody >
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text truncate dangerouslySetInnerHTML={{ __html: item.deskripsi ?? "" }} />
</TableTd>
<TableTd>{item.hierarki}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Text lineClamp={2} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</TableTd>
<TableTd style={{ width: '15%' }}>
<Text>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Group gap="xs">
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</Group>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
))}
)}
</TableTbody>
</Table>
</Box>
@@ -123,9 +146,15 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
my={"md"}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Hapus */}

View File

@@ -2,8 +2,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Center, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core';
import { IconUsers } from '@tabler/icons-react';
import { OrganizationChart } from 'primereact/organizationchart';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
@@ -11,46 +11,50 @@ import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID'
function StrukturOrganisasiPPID() {
return (
<Box >
<Box py="md">
<ListStrukturOrganisasiPPID />
</Box>
);
}
function ListStrukturOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
useEffect(() => {
stateOrganisasi.findMany.load()
}, [])
stateOrganisasi.findMany.load();
}, []);
if (stateOrganisasi.findMany.loading) {
return (
<Center py={40}>
<Loader size="lg" />
</Center>
);
}
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Stack align="center" py={60} gap="sm">
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
<Text fw={500} c="dimmed">Belum ada struktur organisasi yang ditambahkan</Text>
</Stack>
);
}
// Step 1: Group pegawai berdasarkan posisiId
const posisiMap = new Map<string, any>();
for (const pegawai of stateOrganisasi.findMany.data) {
const posisiId = pegawai.posisi.id;
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: []
children: [],
});
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
}
// Step 2: Buat struktur pohon berdasarkan parentId
let root: any[] = [];
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId);
@@ -62,56 +66,64 @@ function ListStrukturOrganisasiPPID() {
}
});
// Step 3: Ubah struktur ke format OrganizationChart
function toOrgChartFormat(node: any): any {
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
status: node.nama,
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
},
children: node.children.map(toOrgChartFormat)
children: node.children.map(toOrgChartFormat),
};
}
const chartData = root.map(toOrgChartFormat);
return (
<Box py={10}>
<Paper bg={colors.grey} p="md" style={{overflowX: 'auto'}}>
<Paper
p="md"
radius="lg"
shadow="md"
withBorder
style={{
overflowX: 'auto',
background: 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)',
}}
>
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
</Paper>
</Box>
);
}
function nodeTemplate(node: any) {
const imageSrc = node?.data?.image || '/img/default.png';
const name = node?.data?.name || 'Tanpa Nama';
const status = node?.data?.status || 'Tidak ada deskripsi';
return (
<Stack align="center" gap={4}>
<Image
src={imageSrc}
alt={name}
radius="xl"
w={120}
h={120}
fit="cover"
/>
<Text fw={600} ta="center">{name}</Text>
<Text size="sm" c="dimmed" ta="center">{status}</Text>
<Stack align="center" gap={6} p="sm" style={{ minWidth: 160 }}>
<Tooltip label={name} position="top" withArrow>
<Image
src={imageSrc}
alt={name}
radius="xl"
w={100}
h={100}
fit="cover"
style={{
border: '1px solid #D3D1D1FF',
}}
/>
</Tooltip>
<Text fw={600} ta="center" size="sm">{name}</Text>
<Text size="xs" c="dimmed" ta="center">{status}</Text>
</Stack>
);
}
export default StrukturOrganisasiPPID;

View File

@@ -1,11 +1,6 @@
'use client'
'use client';
import colors from '@/con/colors';
import {
Box,
Button, Group, Paper,
Stack,
Title
} from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -46,35 +41,52 @@ function VisiMisiPPIDEdit() {
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Visi Misi PPID</Title>
<VisiPPID value={draftVisi} onChange={setDraftVisi} />
<MisiPPID value={draftMisi} onChange={setDraftMisi} />
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={visiMisi.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Visi Misi PPID
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xl">
<Box>
<VisiPPID value={draftVisi} onChange={setDraftVisi} />
</Box>
<Box>
<MisiPPID value={draftMisi} onChange={setDraftMisi} />
</Box>
<Group justify="flex-end" mt="md">
<Button
onClick={submit}
loading={visiMisi.update.loading}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,21 +1,11 @@
'use client'
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Grid,
GridCol,
Image,
Paper,
Skeleton, Stack, Text,
Title
} from '@mantine/core';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
function VisiMisiPPIDList() {
@@ -25,56 +15,99 @@ function VisiMisiPPIDList() {
listVisiMisi.findById.load('1');
}, []);
if (listVisiMisi.findById.loading) {
return (
<Center py={40}>
<Skeleton radius="md" height={800} width="100%" />
</Center>
);
}
if (!listVisiMisi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
<Center py={60}>
<Stack align="center" gap="sm">
<Text fw={500} c="dimmed">Belum ada data visi misi PPID</Text>
</Stack>
</Center>
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack pos={"relative"} gap={"22"}>
<Grid>
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Visi Misi PPID</Title>
<Title order={3} c={colors['blue-button']}>Preview Visi Misi PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}>
<IconEdit size={16} />
</Button>
<Tooltip label="Edit Visi Misi PPID" withArrow>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 'sm', md: 100 }}>
<Grid>
<GridCol span={12}>
<Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
</Center>
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} fw={"bold"}>
</GridCol>
<GridCol span={12}>
<Text ta="center" fz={{ base: '1.2rem', md: '1.8rem' }} fw="bold" c={colors['blue-button']}>
MOTO PPID DESA DARMASABA
</Text>
<Text ta={"center"} fz={{ base: "h4", md: "h3" }} >
<Text ta="center" fz={{ base: '1rem', md: '1.2rem' }} c="dimmed" mt="sm">
MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
</Text>
</Box>
<Box px={{ base: 20, md: 50 }} pb={30}>
<Text ta={"center"} fz={{ base: "h3", md: "h2" }} fw={"bold"}>
VISI PPID
</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }} />
</Box>
<Box px={{ base: 20, md: 50 }}>
<Text ta={"center"} fz={{ base: "h3", md: "h2" }} fw={"bold"}>
MISI PPID
</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }} />
</Box>
</Paper>
</Stack>
</Box>
</GridCol>
</Grid>
<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']}>
VISI PPID
</Text>
<Box
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
style={{
fontSize: '1.1rem',
lineHeight: 1.7,
textAlign: 'justify'
}}
/>
</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']}>
MISI PPID
</Text>
<Box
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
style={{
fontSize: '1.1rem',
lineHeight: 1.7,
textAlign: 'justify'
}}
/>
</Box>
</Box>
</Paper>
</Stack>
</Paper>
);