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,26 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
"use client"; 'use client';
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
import apbdes from "@/app/admin/(dashboard)/_state/landing-page/apbdes"; import colors from '@/con/colors';
import colors from "@/con/colors"; import ApiFetch from '@/lib/api-fetch';
import ApiFetch from "@/lib/api-fetch";
import { import {
Box, Box,
Button, Button,
Group, Group,
Image,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
} from "@mantine/core"; Tooltip,
import { Dropzone } from "@mantine/dropzone"; } from '@mantine/core';
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from "@tabler/icons-react"; import { Dropzone } from '@mantine/dropzone';
import { useParams, useRouter } from "next/navigation"; import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useEffect, useState } from "react"; import { useParams, useRouter } from 'next/navigation';
import { toast } from "react-toastify"; import { useEffect, useState } from 'react';
import { useProxy } from "valtio/utils"; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditAPBDes() { function EditAPBDes() {
const apbdesState = useProxy(apbdes); const apbdesState = useProxy(apbdes);
@@ -38,14 +39,14 @@ function EditAPBDes() {
fileId: apbdesState.edit.form.fileId || '' fileId: apbdesState.edit.form.fileId || ''
}); });
// Load sdgs desa by id saat pertama kali // Load APBDes data by id
useEffect(() => { useEffect(() => {
const loadKolaborasi = async () => { const loadAPBDes = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await apbdesState.edit.load(id); // akses langsung, bukan dari proxy const data = await apbdesState.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
@@ -53,202 +54,238 @@ function EditAPBDes() {
imageId: data.imageId || '', imageId: data.imageId || '',
fileId: data.fileId || '' fileId: data.fileId || ''
}); });
if (data.image) {
if (data?.image?.link) { if (data.image?.link) setPreviewImage(data.image.link);
setPreviewImage(data.image.link); if (data.file?.link) setPreviewDoc(data.file.link);
}
}
if (data.file) {
if (data?.file?.link) {
setPreviewDoc(data.file.link);
}
}
} }
} catch (error) { } catch (error) {
console.error("Error loading apbdes:", error); console.error('Error loading APBDes:', error);
toast.error("Gagal memuat data apbdes"); toast.error('Gagal memuat data APBDes');
} }
}; };
loadKolaborasi(); loadAPBDes();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// edit global state with form data // Update global state with form data
apbdesState.edit.form = { apbdesState.edit.form = {
...apbdesState.edit.form, ...apbdesState.edit.form,
name: formData.name, ...formData,
jumlah: formData.jumlah,
imageId: formData.imageId // Keep existing imageId if not changed
}; };
// Jika ada file image baru, upload // Upload new image if exists
if (imageFile) { if (imageFile) {
const res = await ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }); const res = await ApiFetch.api.fileStorage.create.post({
file: imageFile,
name: imageFile.name
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal upload gambar');
} }
// edit imageId in global state
apbdesState.edit.form.imageId = uploaded.id; apbdesState.edit.form.imageId = uploaded.id;
} }
// Jika ada file doc baru, upload // Upload new document if exists
if (docFile) { if (docFile) {
const res = await ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }); const res = await ApiFetch.api.fileStorage.create.post({
file: docFile,
name: docFile.name
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload doc"); return toast.error('Gagal upload dokumen');
} }
// edit fileId in global state
apbdesState.edit.form.fileId = uploaded.id; apbdesState.edit.form.fileId = uploaded.id;
} }
await apbdesState.edit.update(); await apbdesState.edit.update();
toast.success("apbdes berhasil diperbarui!"); toast.success('APBDes berhasil diperbarui!');
router.push("/admin/landing-page/APBDes"); router.push('/admin/landing-page/APBDes');
} catch (error) { } catch (error) {
console.error("Error updating apbdes:", error); console.error('Error updating APBDes:', error);
toast.error("Terjadi kesalahan saat memperbarui apbdes"); toast.error('Terjadi kesalahan saat memperbarui APBDes');
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit APBDes</Title> Edit APBDes
</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 <TextInput
label="Nama APBDes"
placeholder="Masukkan nama APBDes"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} required
placeholder="masukkan nama"
/> />
<TextInput <TextInput
label="Jumlah Anggaran"
placeholder="Masukkan jumlah anggaran"
value={formData.jumlah} value={formData.jumlah}
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })} onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>} required
placeholder="masukkan jumlah"
/> />
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setImageFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
}}
>
<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>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih image
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Image</Text>
{previewImage ? (
<iframe
src={previewImage}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Doc</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar APBDes
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setDocFile(selectedFile); if (selectedFile) {
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview setImageFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ maxSize={5 * 1024 ** 2}
'application/*': ['.pdf', '.doc', '.docx'], accept={{ 'image/*': [] }}
}} radius="md"
> p="xl"
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> >
<Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Accept> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
<Dropzone.Reject> </Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Reject> <IconX size={48} color="red" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Reject>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <Dropzone.Idle>
</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> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
Drag file ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format doc radius="md"
</Text> style={{
</div> maxHeight: 300,
</Group> objectFit: 'contain',
</Dropzone> border: `1px solid ${colors['blue-button']}`
<Box> }}
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> />
{previewDoc ? (
<iframe
src={previewDoc}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada doc tersedia</Text>
)}
</Box> </Box>
</Box> )}
</Box> </Box>
<Button onClick={handleSubmit}>Simpan</Button>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Dokumen APBDes
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setDocFile(selectedFile);
setPreviewDoc(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={10 * 1024 ** 2} // 10MB
accept={{
'application/pdf': ['.pdf'],
'application/msword': ['.doc'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
'application/vnd.ms-excel': ['.xls'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
}}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={150}>
<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>
<IconFile size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret dokumen atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX
</Text>
</Stack>
</Group>
</Dropzone>
{previewDoc && (
<Box mt="sm">
<Text size="sm" c="dimmed" mb="xs">
Dokumen terpilih: {docFile?.name || 'Dokumen'}
</Text>
<Button
component="a"
href={previewDoc}
target="_blank"
rel="noopener noreferrer"
variant="light"
leftSection={<IconFile size={16} />}
size="sm"
>
Lihat Dokumen
</Button>
</Box>
)}
</Box>
<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
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,9 +1,9 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { ActionIcon, 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 { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconFile, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconFile, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -35,89 +35,125 @@ function DetailAPBDes() {
if (!apbdesState.findUnique.data) { if (!apbdesState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
const data = apbdesState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail APBDes</Text> </Button>
{apbdesState.findUnique.data ? (
<Paper key={apbdesState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: "100%", md: "60%" }}
<Text fw={"bold"} fz={"lg"}>Nama APBDes</Text> bg={colors['white-1']}
<Text fz={"lg"}>{apbdesState.findUnique.data?.name}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fw={"bold"} fz={"lg"}>Jumlah</Text> >
<Text fz={"lg"}>{apbdesState.findUnique.data?.jumlah}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail APBDes
<Text fw={"bold"} fz={"lg"}>Gambar</Text> </Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={apbdesState.findUnique.data?.image?.link} alt="gambar" />
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="md">
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> <Box>
{apbdesState.findUnique.data?.file?.link ? ( <Text fz="lg" fw="bold">Nama APBDes</Text>
<ActionIcon <Text fz="md" c="dimmed">{data.name || '-'}</Text>
component="a" </Box>
href={apbdesState.findUnique.data.file.link}
target="_blank" <Box>
rel="noopener noreferrer" <Text fz="lg" fw="bold">Jumlah Anggaran</Text>
variant="transparent" <Text fz="md" c="dimmed">Rp. {data.jumlah || '-'}</Text>
> </Box>
<IconFile size={25} color={colors['blue-button']}/>
</ActionIcon> <Box>
) : ( <Text fz="lg" fw="bold">Gambar</Text>
<Text>Tidak ada dokumen tersedia</Text> {data.image?.link ? (
)} <Image
</Box> src={data.image.link}
<Flex gap={"xs"} mt={10}> alt={data.name || 'Gambar APBDes'}
w={200}
height={150}
radius="md"
fit="contain"
style={{ border: '1px solid #ddd' }}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Dokumen</Text>
{data.file?.link ? (
<Button <Button
onClick={() => { component="a"
if (apbdesState.findUnique.data) { href={data.file.link}
setSelectedId(apbdesState.findUnique.data.id); target="_blank"
setModalHapus(true); rel="noopener noreferrer"
} variant="light"
}} leftSection={<IconFile size={18} />}
disabled={apbdesState.delete.loading || !apbdesState.findUnique.data} size="sm"
color={"red"} mt="xs"
> >
<IconX size={20} /> Lihat Dokumen
</Button> </Button>
) : (
<Text fz="sm" c="dimmed">Tidak ada dokumen</Text>
)}
</Box>
<Group gap="sm" mt="md">
<Tooltip label="Hapus APBDes" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (apbdesState.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/landing-page/APBDes/${apbdesState.findUnique.data.id}/edit`); setModalHapus(true);
}
}} }}
disabled={!apbdesState.findUnique.data} disabled={apbdesState.delete.loading}
color={"green"} variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit APBDes" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/APBDes/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus APBDes ini?' text="Apakah Anda yakin ingin menghapus APBDes ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,10 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -70,140 +81,168 @@ function CreateAPBDes() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah APBDes
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create APBDes</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Gambar APBDes */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Image</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar APBDes
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setImageFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setImageFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ maxSize={5 * 1024 ** 2}
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'], accept={{ 'image/*': [] }}
}} radius="md"
> p="xl"
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> >
<Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Accept> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
<Dropzone.Reject> </Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Reject> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Reject>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Idle> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Box>
<Text size="xl" inline>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline display="block" mt={7}>
Maksimal 5MB (format: JPEG, JPG, PNG, GIF, WEBP, SVG)
</Text>
</Box>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="md" style={{ textAlign: 'center' }}>
Drag file ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format image radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> />
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewImage ? (
<iframe
src={previewImage}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box> </Box>
</Box> )}
</Box> </Box>
{/* Dokumen APBDes */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Dokumen</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Dokumen APBDes
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setDocFile(selectedFile); if (selectedFile) {
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview setDocFile(selectedFile);
} setPreviewDoc(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format PDF, DOC, atau DOCX')}
accept={{ maxSize={5 * 1024 ** 2}
'application/*': ['.pdf', '.doc', '.docx'], accept={{
}} 'application/pdf': ['.pdf'],
> 'application/msword': ['.doc'],
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
<Dropzone.Accept> }}
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> radius="md"
</Dropzone.Accept> p="xl"
<Dropzone.Reject> >
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Group justify="center" gap="xl" mih={180}>
</Dropzone.Reject> <Dropzone.Accept>
<Dropzone.Idle> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> </Dropzone.Accept>
</Dropzone.Idle> <Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Box>
<Text size="xl" inline>
Seret dokumen atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline display="block" mt={7}>
Maksimal 5MB (format: PDF, DOC, DOCX)
</Text>
</Box>
</Group>
</Dropzone>
<div> {previewDoc && (
<Text size="xl" inline> <Box mt="md">
Drag file ke sini atau klik untuk pilih file <Text fw="bold" fz="sm" mb={6}>
</Text> Pratinjau Dokumen
<Text size="sm" c="dimmed" inline mt={7}> </Text>
Maksimal 5MB dan harus format dokumen <iframe
</Text> src={previewDoc}
</div> width="100%"
</Group> height="500px"
</Dropzone> style={{ border: '1px solid #ddd', borderRadius: '8px' }}
<Box> />
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewDoc ? (
<iframe
src={previewDoc}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box> </Box>
</Box> )}
</Box> </Box>
{/* Form Input */}
<TextInput <TextInput
value={stateAPBDes.create.form.name} label="Nama APBDes"
onChange={(val) => { placeholder="Masukkan nama APBDes"
stateAPBDes.create.form.name = val.target.value; value={stateAPBDes.create.form.name || ''}
}} onChange={(e) => (stateAPBDes.create.form.name = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} required
placeholder='Masukkan judul'
/> />
<TextInput <TextInput
value={stateAPBDes.create.form.jumlah} label="Jumlah Anggaran"
onChange={(val) => { placeholder="Masukkan jumlah anggaran"
stateAPBDes.create.form.jumlah = val.target.value; value={stateAPBDes.create.form.jumlah || ''}
}} onChange={(e) => (stateAPBDes.create.form.jumlah = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>} required
placeholder='Masukkan jumlah'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</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)',
}}
disabled={!imageFile || !docFile || !stateAPBDes.create.form.name || !stateAPBDes.create.form.jumlah}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; 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 { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import apbdes from '../../_state/landing-page/apbdes'; import apbdes from '../../_state/landing-page/apbdes';
@@ -17,7 +16,7 @@ function APBDes() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='APBDes' title='APBDes'
placeholder='pencarian' placeholder='Cari APBDes...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -48,73 +47,102 @@ function ListAPBDes({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar APBDes</Title>
title='List APBDes' <Tooltip label="Tambah APBDes" withArrow>
href='/admin/landing-page/APBDes/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: 'auto' }}> color="blue"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() => router.push('/admin/landing-page/APBDes/create')}
<TableTr> >
<TableTh>Nama APBDes</TableTh> Tambah Baru
<TableTh>Jumlah APBDes</TableTh> </Button>
<TableTh>Document</TableTh> </Tooltip>
<TableTh>Detail</TableTh> </Group>
</TableTr>
</TableThead> <Box style={{ overflowX: 'auto' }}>
<TableTbody> <Table highlightOnHover>
{filteredData.map((item) => ( <TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama APBDes</TableTh>
<TableTh style={{ width: '25%' }}>Jumlah</TableTh>
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end">{item.name}</Text>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate="end" fz={"sm"}>Rp. {item.jumlah}</Text> <Text>Rp. {item.jumlah}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
{item.file?.link ? ( {item.file?.link ? (
<ActionIcon <Button
component="a" component="a"
href={item.file.link} href={item.file.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
variant='transparent' variant="light"
leftSection={<IconFile size={18} />}
size="sm"
> >
<IconFile size={25} color={colors['blue-button']} /> Lihat Dokumen
</ActionIcon> </Button>
) : ( ) : (
<Text>Tidak ada dokumen tersedia</Text> <Text c="dimmed" fz="sm">Tidak ada dokumen</Text>
)} )}
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}
fullWidth
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Center mt="md">
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
my={"md"} color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -65,7 +65,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow> <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab <TabsTab
value={tab.value} value={tab.value}
leftSection={tab.icon} leftSection={tab.icon}

View File

@@ -74,7 +74,7 @@ export default function EditKategoriDesaAntiKorupsi() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -35,7 +35,7 @@ export default function CreateKategoriDesaAntiKorupsi() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -99,7 +99,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
<Tooltip label="Edit" withArrow> <Tooltip label="Edit" withArrow>
<Button <Button
variant="light" variant="light"
color="blue" color="green"
size="sm" size="sm"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)} onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
> >

View File

@@ -117,7 +117,7 @@ export default function EditDesaAntiKorupsi() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -58,6 +58,7 @@ export default function DetailKegiatanDesa() {
p="lg" p="lg"
radius="md" radius="md"
shadow="sm" shadow="sm"
withBorder
> >
<Stack gap="md"> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}> <Text fz="2xl" fw="bold" c={colors['blue-button']}>

View File

@@ -87,7 +87,7 @@ export default function CreateDesaAntiKorupsi() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; import { 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 { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -14,9 +14,9 @@ function DesaAntiKorupsi() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Desa Anti Korupsi' title="Program Desa Anti Korupsi"
placeholder='Cari nama program atau kategori...' placeholder="Cari nama program atau kategori..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={18} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
@@ -28,14 +28,7 @@ function DesaAntiKorupsi() {
function ListDesaAntiKorupsi({ search }: { search: string }) { function ListDesaAntiKorupsi({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const listState = useProxy(korupsiState.desaAntikorupsi); const listState = useProxy(korupsiState.desaAntikorupsi);
const { data, page, totalPages, loading, load } = listState.findMany;
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, search);
@@ -45,90 +38,117 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={650} radius="lg" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py="md">
<Paper p="lg" radius="lg" shadow="md" withBorder>
<Stack align="center" gap="sm">
<Title order={4}>Data Program Desa Anti Korupsi</Title>
<Text c="dimmed" ta="center">
Belum ada data program yang tersedia
</Text>
</Stack>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box>
<Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Stack gap="md">
<Group justify="space-between" mb="md"> <Paper p="lg" radius="lg" shadow="md" withBorder>
<Title order={4}>Daftar Program Desa Anti Korupsi</Title> <Group justify="space-between" mb="md">
<Tooltip label="Tambah Program Desa Anti Korupsi" withArrow> <Title order={4}>Daftar Program Desa Anti Korupsi</Title>
<Button <Tooltip label="Tambah Program Baru" withArrow>
leftSection={<IconPlus size={18} />} <Button
color="blue" leftSection={<IconPlus size={18} />}
variant="light" color="blue"
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')} onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Table
striped
highlightOnHover
withRowBorders
verticalSpacing="sm"
> >
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Program</TableTh> <TableTh style={{ width: '50%' }}>Nama Program</TableTh>
<TableTh>Kategori</TableTh> <TableTh style={{ width: '30%' }}>Kategori</TableTh>
<TableTh>Aksi</TableTh> <TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '50%' }}>
<Box w={350}> <Text fw={500} lineClamp={1}>
<Text lineClamp={1} fw={500}>{item.name || '-'}</Text> {item.name || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '30%' }}>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
{item.kategori?.name || '-'} {item.kategori?.name || '-'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '20%', textAlign: 'center' }}>
<Button <Button
size="xs"
radius="md"
variant="light" variant="light"
color="blue" color="blue"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)} leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`
)
}
> >
<IconDeviceImacCog size={20} /> Detail
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={3}>
<Center py={20}> <Text ta="center" c="dimmed">
<Text c="dimmed">Tidak ada data program yang cocok</Text> Tidak ditemukan data dengan kata kunci pencarian
</Center> </Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
)} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Paper>
</Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { total={totalPages}
load(newPage, 10); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" size="md"
mb="md" radius="md"
color="blue" mt="md"
radius="md" />
/> </Center>
</Center> </Stack>
</Box> </Box>
); );
} }

View File

@@ -1,63 +1,89 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { IconChartBar, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) { function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "Grafik Kepuasan Masyarakat", label: "Grafik Kepuasan",
value: "grafikkepuasannamasyarakat", description: "Lihat visualisasi grafik kepuasan masyarakat",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat" value: "grafik",
}, href: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat",
{ icon: <IconChartBar size={18} stroke={1.8} />
label: "Responden", },
value: "responden", {
href: "/admin/landing-page/indeks-kepuasan-masyarakat/responden" label: "Responden",
}, description: "Kelola dan tinjau data responden",
value: "responden",
]; href: "/admin/landing-page/indeks-kepuasan-masyarakat/responden",
const curentTab = tabs.find(tab => tab.href === pathname) icon: <IconUsers size={18} stroke={1.8} />
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); },
];
const handleTabChange = (value: string | null) => { const curentTab = tabs.find(tab => tab.href === pathname);
const tab = tabs.find(t => t.value === value) const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => { const handleTabChange = (value: string | null) => {
const match = tabs.find(tab => tab.href === pathname) const tab = tabs.find(t => t.value === value);
if (match) { if (tab) router.push(tab.href);
setActiveTab(match.value) setActiveTab(value);
} };
}, [pathname])
return ( useEffect(() => {
<Stack> const match = tabs.find(tab => tab.href === pathname);
<Title order={3}>Indeks Kepuasan Masyarakat</Title> if (match) setActiveTab(match.value);
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> }, [pathname]);
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => ( return (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> <Stack gap="lg">
))} <Title order={2} style={{ fontWeight: 700, color: "#1a1a1a" }}>
</TabsList> Indeks Kepuasan Masyarakat
{tabs.map((e, i) => ( </Title>
<TabsPanel key={i} value={e.value}> <Tabs
{/* Konten dummy, bisa diganti tergantung routing */} radius="xl"
<></> color="blue"
</TabsPanel> variant="pills"
))} value={activeTab}
</Tabs> onChange={handleTabChange}
{children} >
</Stack> <TabsList
); p="sm"
style={{
background: "#F3F4FB",
borderRadius: "1rem",
}}
>
{tabs.map((e, i) => (
<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} mt="md">
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
} }
export default LayoutTabsKepuasan; export default LayoutTabsKepuasan;

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { PieChart, BarChart } from '@mantine/charts'; // ✅ Ganti recharts dengan Mantine import { PieChart, BarChart } from '@mantine/charts';
import { import {
Box, Box,
Center, Center,
@@ -22,11 +22,8 @@ interface ChartDataItem {
name: string; name: string;
value: number; value: number;
color: string; color: string;
label?: string;
} }
function Page() { function Page() {
const state = useProxy(indeksKepuasanState.responden); const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany; const { data, loading } = state.findMany;
@@ -41,28 +38,23 @@ function Page() {
return; return;
} }
if (data) { if (data) {
// Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length; const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length; const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
// Hitung total berdasarkan rating
const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length; const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length; const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length; const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length; const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
// Hitung total berdasarkan kelompok umur
const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length; const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length; const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length; const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
// Update gender chart data
setDonutDataJenisKelamin([ setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] }, { name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' }, { name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]); ]);
// Update rating chart data
setDonutDataRating([ setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] }, { name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' }, { name: 'Baik', value: totalBaik, color: '#10A85AFF' },
@@ -70,18 +62,15 @@ function Page() {
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' }, { name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
]); ]);
// Update age group chart data
setDonutDataKelompokUmur([ setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] }, { name: 'Muda', value: totalMuda, color: colors['blue-button'] },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' }, { name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' }, { name: 'Lansia', value: totalLansia, color: '#FFA500' },
]); ]);
// Process data for bar chart (group by month)
const monthYearMap = new Map<string, number>(); const monthYearMap = new Map<string, number>();
data.forEach((item: any) => { data.forEach((item: any) => {
// Try both createdAt and tanggal fields
const dateValue = item.tanggal || item.createdAt; const dateValue = item.tanggal || item.createdAt;
if (!dateValue) return; if (!dateValue) return;
@@ -95,7 +84,6 @@ function Page() {
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1); monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
}); });
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries()) const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => { .map(([key, count]) => {
const [year, month] = key.split('-'); const [year, month] = key.split('-');
@@ -104,7 +92,7 @@ function Page() {
return { return {
month: `${monthName} ${year}`, month: `${monthName} ${year}`,
count, count,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10) sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10),
}; };
}) })
.sort((a, b) => a.sortKey - b.sortKey) .sort((a, b) => a.sortKey - b.sortKey)
@@ -116,28 +104,27 @@ function Page() {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="xl">
<Skeleton height={730} /> <Skeleton height={750} radius="lg" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Stack py={10}> <Stack py="xl">
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" size="lg">
Belum ada data untuk ditampilkan Belum ada data yang tersedia
</Text> </Text>
</Stack> </Stack>
); );
} }
return ( return (
<Stack gap="xs"> <Stack gap="lg">
{/* Bar Chart - Data per Tanggal */} <Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Paper bg={colors['white-1']} p="md" radius="md" mb="md"> <Title order={3} mb="md" ta="center">Tren Jumlah Responden</Title>
<Title order={4} mb="md" ta="center">Jumlah Responden per Bulan</Title> <Box h={320}>
<Box h={300}>
<BarChart <BarChart
h={300} h={300}
data={barChartData} data={barChartData}
@@ -152,14 +139,13 @@ function Page() {
</Box> </Box>
</Paper> </Paper>
<SimpleGrid cols={{ base: 1, md: 3 }}> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{/* Chart Jenis Kelamin */} <Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Jenis Kelamin</Title> <Title order={4} ta="center">Distribusi Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? ( {donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Tidak ada data
</Text> </Text>
) : ( ) : (
<Box> <Box>
@@ -168,15 +154,15 @@ function Page() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={250} size={220}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
<Stack gap="sm" mt="md"> <Stack gap="xs" mt="md">
{donutDataJenisKelamin.map((entry) => ( {donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="sm" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -185,13 +171,12 @@ function Page() {
</Stack> </Stack>
</Paper> </Paper>
{/* Chart Rating */} <Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Pilihan</Title> <Title order={4} ta="center">Distribusi Penilaian</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Tidak ada data
</Text> </Text>
) : ( ) : (
<Box> <Box>
@@ -200,15 +185,15 @@ function Page() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={250} size={220}
data={donutDataRating} data={donutDataRating}
/> />
</Center> </Center>
<Stack gap="sm" mt="md"> <Stack gap="xs" mt="md">
{donutDataRating.map((entry) => ( {donutDataRating.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="sm" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -217,13 +202,12 @@ function Page() {
</Stack> </Stack>
</Paper> </Paper>
{/* Chart Kelompok Umur */} <Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Umur</Title> <Title order={4} ta="center">Distribusi Kelompok Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? ( {donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Tidak ada data
</Text> </Text>
) : ( ) : (
<Box> <Box>
@@ -232,15 +216,15 @@ function Page() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={250} size={220}
data={donutDataKelompokUmur} data={donutDataKelompokUmur}
/> />
</Center> </Center>
<Stack gap="sm" mt="md"> <Stack gap="xs" mt="md">
{donutDataKelompokUmur.map((entry) => ( {donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="sm" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -253,4 +237,4 @@ function Page() {
); );
} }
export default Page; export default Page;

View File

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

View File

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

View File

@@ -1,22 +1,37 @@
'use client'; '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';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan'; import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
function Responden() { function Responden() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Responden' title="Data Responden"
placeholder='pencarian' placeholder="Cari nama responden..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={18} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
@@ -34,84 +49,99 @@ function ListResponden({ search }: ListRespondenProps) {
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10);
}, [page]); }, [page]);
const filteredData = (data || []).filter((item) => {
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase(); const keyword = search.toLowerCase();
return ( return item.name.toLowerCase().includes(keyword);
item.name.toLowerCase().includes(keyword)
);
}); });
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={730} /> <Skeleton height={650} radius="lg" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py={10}> <Box py="md">
<Paper p="md"> <Paper p="lg" radius="lg" shadow="md" withBorder>
<Stack> <Stack align="center" gap="sm">
<Title>Responden</Title> <Title order={4}>Data Responden</Title>
<Table striped withTableBorder withRowBorders> <Text c="dimmed" ta="center">
<TableThead> Belum ada data responden yang tersedia
<TableTr> </Text>
<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> </Stack>
</Paper> </Paper>
</Box > </Box>
); );
} }
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="md">
<Paper bg={colors['white-1']} p="md" h={{ base: 730, md: 650 }}> <Paper p="lg" radius="lg" shadow="md" withBorder>
<Title mb={10} order={3}>List Responden</Title> <Title order={4} mb="sm">
<Table striped withTableBorder withRowBorders> Daftar Responden
</Title>
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
verticalSpacing="sm"
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> <TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Nama</TableTh> <TableTh style={{ width: '25%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh> <TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh> <TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh> <TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTd colSpan={5}>
<Text ta='center' c='dimmed'>Belum ada data responden</Text> <Text ta="center" c="dimmed">
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
) : ( ) : (
filteredData.map((item, index) => ( filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTd ta="center">{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.name}</TableTd> <TableTd ta="center">{item.name}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.tanggal <TableTd ta="center">
? new Date(item.tanggal).toLocaleDateString('id-ID') {item.tanggal
: '-'}</TableTd> ? new Date(item.tanggal).toLocaleDateString('id-ID', {
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.jenisKelamin.name}</TableTd> day: '2-digit',
<TableTd style={{ width: '15%', textAlign: 'center' }}> month: 'long',
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}> year: 'numeric',
<IconDeviceImac size={20} /> })
: '-'}
</TableTd>
<TableTd ta="center">{item.jenisKelamin.name}</TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
)
}
>
Detail
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -123,22 +153,19 @@ function ListResponden({ search }: ListRespondenProps) {
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
total={totalPages}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} size="md"
radius="md"
mt="md" mt="md"
mb="md"
/> />
</Center> </Center>
</Stack> </Stack>
</Box> </Box>
); );
} }
export default Responden; export default Responden;

View File

@@ -1,62 +1,103 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconCategory, IconListDetails } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "List Prestasi Desa",
value: "listPrestasiDesa",
href: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
},
{
label: "Kategori Prestasi Desa",
value: "kategoriPrestasiDesa",
href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "Daftar Prestasi",
router.push(tab.href) value: "listPrestasiDesa",
} href: "/admin/landing-page/prestasi-desa/list-prestasi-desa",
setActiveTab(value) icon: <IconListDetails size={18} stroke={1.8} />,
tooltip: "Kelola daftar prestasi desa",
},
{
label: "Kategori Prestasi",
value: "kategoriPrestasiDesa",
href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori prestasi desa",
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
} }
setActiveTab(value);
};
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Prestasi Desa</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Prestasi Desa
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
))} value={activeTab}
</TabsList> onChange={handleTabChange}
{tabs.map((e, i) => ( radius="lg"
<TabsPanel key={i} value={e.value}> keepMounted={false}
{/* Konten dummy, bisa diganti tergantung routing */} >
<></> <TabsList
</TabsPanel> p="sm"
))} style={{
</Tabs> 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((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} {children}
</Stack> </TabsPanel>
); ))}
</Tabs>
</Stack>
);
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa'; import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -45,7 +45,7 @@ function EditKategoriPrestasi() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (!formData.name.trim()) { if (!formData.name.trim()) {
toast.error('Nama kategori prestasi desa tidak boleh kosong'); toast.error('Nama kategori prestasi tidak boleh kosong');
return; return;
} }
@@ -70,24 +70,48 @@ function EditKategoriPrestasi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Prestasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Kategori Prestasi Desa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori Prestasi"
placeholder="Masukkan nama kategori prestasi"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>} required
placeholder='Masukkan nama kategori prestasi desa'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa'; import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -30,31 +30,51 @@ function CreateKategoriPrestasi() {
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box> <Group mb="md">
<Box mb={10}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Prestasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Kategori Prestasi Desa</Title> bg={colors['white-1']}
<TextInput p="lg"
value={stateKategori.create.form.name} radius="md"
onChange={(val) => { shadow="sm"
stateKategori.create.form.name = val.target.value; style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Kategori Prestasi"
placeholder="Masukkan nama kategori prestasi"
value={stateKategori.create.form.name || ''}
onChange={(val) => (stateKategori.create.form.name = val.target.value)}
required
/>
<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)',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>} >
placeholder='Masukkan nama kategori prestasi desa' Simpan
/> </Button>
<Group> </Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> </Stack>
</Group> </Paper>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; 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 { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import prestasiState from '../../../_state/landing-page/prestasi-desa'; import prestasiState from '../../../_state/landing-page/prestasi-desa';
@@ -18,7 +17,7 @@ function KategoriPrestasiDesa() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Prestasi Desa' title='Kategori Prestasi Desa'
placeholder='pencarian' placeholder='Cari kategori prestasi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -65,60 +64,100 @@ function ListKategoriPrestasi({ search }: { search: string }) {
} }
return ( return (
<Box py={10}> <Box>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
<JudulList <Group justify="space-between" mb="md">
title='List Kategori Prestasi Desa' <Title order={4} c="dark">List Kategori Prestasi</Title>
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create' <Tooltip label="Tambah Kategori Prestasi" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
<Box style={{ overflowY: "auto" }}> Tambah Baru
<Table striped withTableBorder withRowBorders> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table verticalSpacing="sm" highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Kategori</TableTh> <TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length === 0 ? (
<TableTr key={item.id}> <TableTr>
<TableTd>{item.name}</TableTd> <TableTd colSpan={2} style={{ textAlign: 'center' }}>
<TableTd> <Text py="md" c="dimmed">
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}> {search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
<IconEdit size={20} /> </Text>
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd style={{ textAlign: 'center', width: '120px' }}>
<Tooltip label="Edit" withArrow position="top">
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd style={{ textAlign: 'center', width: '120px' }}>
<Tooltip label="Hapus" withArrow position="top">
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{totalPages > 1 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
withEdges
size="sm"
styles={{
control: {
'&[data-active]': {
background: `${colors['blue-button']} !important`,
},
},
}}
/>
</Center>
)}
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori prestasi desa ini?' text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
/> />
</Box> </Box >
); );
} }

View File

@@ -2,8 +2,8 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -109,122 +109,144 @@ function EditPrestasiDesa() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> </Tooltip>
<Stack> <Title order={4} ml="sm" c="dark">
<Text fz={"xl"} fw={"bold"}>Edit List Prestasi Desa</Text> Edit Prestasi Desa
{editState.findUnique.data ? ( </Title>
<Paper key={editState.findUnique.data.id}> </Group>
<Stack gap={"xs"}>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val
})
}}
/>
</Box>
<Select
value={formData.kategoriId}
onChange={(val) => {
setFormData({
...formData,
kategoriId: val ?? ""
})
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori"
data={
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
/>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Stack gap={"xs"}>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.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>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <Paper
<Text size="xl" inline> w={{ base: '100%', md: '50%' }}
Drag file ke sini atau klik untuk pilih file bg={colors['white-1']}
</Text> p="lg"
<Text size="sm" c="dimmed" inline mt={7}> radius="md"
Maksimal 5MB dan harus format image shadow="sm"
</Text> style={{ border: '1px solid #e0e0e0' }}
</div> >
</Group> <Stack gap="md">
</Dropzone> <TextInput
<Box> label="Judul Prestasi"
<Text fw={"bold"} fz={"lg"}>Image</Text> placeholder="Masukkan judul prestasi"
{previewFile ? ( value={formData.name}
<Image onChange={(val) => {
alt='' setFormData({
src={previewFile} ...formData,
width="100%" name: val.target.value
height="500px" });
style={{ border: "1px solid #ccc", borderRadius: "8px" }} }}
/> required
) : ( />
<Text>Tidak ada image tersedia</Text>
)} <Box>
</Box> <Text fw="bold" fz="sm" mb={6}>
</Stack> Deskripsi
</Box> </Text>
<Group> <EditEditor
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> value={formData.deskripsi}
</Group> onChange={(val) => {
</Stack> setFormData({
</Paper> ...formData,
) : null} deskripsi: val
});
}}
/>
</Box>
<Select
label="Kategori"
placeholder="Pilih kategori"
value={formData.kategoriId}
onChange={(val) => {
setFormData({
...formData,
kategoriId: val ?? ""
});
}}
data={
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Prestasi
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
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>
{previewFile && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewFile}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
/>
</Box>
)}
</Box>
<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
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditPrestasiDesa; export default EditPrestasiDesa;

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa'; import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, 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 { 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 { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -40,45 +40,69 @@ function DetailPrestasiDesa() {
} }
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail List Prestasi Desa</Text> </Button>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: "100%", md: "60%" }}
<Text fw={"bold"} fz={"lg"}>Judul</Text> bg={colors['white-1']}
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> >
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} /> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail Prestasi Desa
<Text fw={"bold"} fz={"lg"}>Kategori</Text> </Text>
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Image</Text> <Box>
{detailState.findUnique.data?.image?.link ? ( <Text fz="lg" fw="bold">Judul</Text>
<iframe <Text fz="md" c="dimmed">{detailState.findUnique.data?.name || '-'}</Text>
src={detailState.findUnique.data.image.link} </Box>
width="100%"
height="500px" <Box>
style={{ border: "1px solid #ccc", borderRadius: "8px" }} <Text fz="lg" fw="bold">Kategori</Text>
/> <Text fz="md" c="dimmed">{detailState.findUnique.data?.kategori?.name || '-'}</Text>
) : ( </Box>
<Text>Tidak ada image tersedia</Text>
)} <Box>
</Box> <Text fz="lg" fw="bold">Deskripsi</Text>
<Flex gap={"xs"} mt={10}> <Box
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{detailState.findUnique.data?.image?.link ? (
<Image
src={detailState.findUnique.data.image.link}
alt={detailState.findUnique.data.name || 'Gambar Prestasi'}
w={300}
fit="contain"
style={{ borderRadius: '8px', border: '1px solid #e0e0e0' }}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Prestasi" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id); setSelectedId(detailState.findUnique.data.id);
@@ -86,25 +110,33 @@ function DetailPrestasiDesa() {
} }
}} }}
disabled={detailState.delete.loading || !detailState.findUnique.data} disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"} variant="light"
radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Prestasi" withArrow position="top">
<Button <Button
color="green"
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${detailState.findUnique.data.id}/edit`); router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,13 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa'; import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, 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 { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -60,103 +59,132 @@ function CreatePrestasiDesa() {
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa") router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa")
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Prestasi Desa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Prestasi Desa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Image</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Prestasi
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewFile(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid. Hanya file gambar yang diperbolehkan.')}
accept={{ maxSize={5 * 1024 ** 2} // 5MB
'application/*': ['.jpg', '.jpeg', '.png'], accept={{
}} 'image/*': ['.jpg', '.jpeg', '.png', '.webp'],
> }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> >
<Dropzone.Accept> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Accept> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<Dropzone.Reject> </Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Reject> <IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Reject>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Idle> <IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file Seret file gambar ke sini atau klik untuk memilih
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image Unggah file gambar (maks. 5MB)
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Image</Text> {previewFile && (
{previewFile ? ( <Box mt="md">
<iframe <Text size="sm" fw={500} mb={4}>
Pratinjau Gambar:
</Text>
<Box style={{ maxWidth: '100%', maxHeight: '300px', overflow: 'hidden', borderRadius: '8px' }}>
<Image
src={previewFile} src={previewFile}
width="100%" alt="Pratinjau gambar prestasi"
height="500px" fit="cover"
style={{ border: "1px solid #ccc", borderRadius: "8px" }} style={{ width: '100%', height: 'auto' }}
/> />
) : ( </Box>
<Text>Tidak ada image tersedia</Text>
)}
</Box> </Box>
</Box> )}
</Box> </Box>
<TextInput <TextInput
label="Judul Prestasi"
placeholder="Masukkan judul prestasi"
value={stateCreate.create.form.name} value={stateCreate.create.form.name}
onChange={(val) => { onChange={(e) => (stateCreate.create.form.name = e.target.value)}
stateCreate.create.form.name = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw={500} fz="sm" mb={6}>
Deskripsi Prestasi
</Text>
<CreateEditor <CreateEditor
value={stateCreate.create.form.deskripsi} value={stateCreate.create.form.deskripsi}
onChange={(val) => { onChange={(val) => (stateCreate.create.form.deskripsi = val)}
stateCreate.create.form.deskripsi = val;
}}
/> />
</Box> </Box>
<Select <Select
value={stateCreate.create.form.kategoriId} label="Kategori Prestasi"
onChange={(val) => {
stateCreate.create.form.kategoriId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={stateCreate.create.form.kategoriId}
onChange={(val) => (stateCreate.create.form.kategoriId = val ?? '')}
data={ data={
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({ prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name, label: v.name,
})) || [] })) || []
} }
required
/> />
<Group> <Group justify="flex-end" mt="md">
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Button
variant="light"
color="gray"
onClick={() => router.back()}
style={{ marginRight: 'auto' }}
>
Batal
</Button>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
style={{ boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)' }}
>
Simpan Prestasi
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,23 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import prestasiState from '../../../_state/landing-page/prestasi-desa'; import prestasiState from '../../../_state/landing-page/prestasi-desa';
function ListPrestasiDesa() { function ListPrestasiDesa() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Prestasi Desa' title='Prestasi Desa'
placeholder='pencarian' placeholder='Cari nama prestasi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +29,7 @@ function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa) const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter(); const router = useRouter();
const{ const {
data, data,
page, page,
totalPages, totalPages,
@@ -62,63 +60,88 @@ function ListPrestasi({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Prestasi Desa</Title>
title='List Prestasi Desa' <Tooltip label="Tambah Prestasi" withArrow>
href='/admin/landing-page/prestasi-desa/list-prestasi-desa/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: 'auto' }}> color="blue"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
<TableTr> >
<TableTh>Nama Prestasi Desa</TableTh> Tambah Baru
<TableTh>Deskripsi Prestasi Desa</TableTh> </Button>
<TableTh>Kategori Prestasi Desa</TableTh> </Tooltip>
<TableTh>Detail</TableTh> </Group>
</TableTr> <Box style={{ overflowX: 'auto' }}>
</TableThead> <Table highlightOnHover>
<TableTbody> <TableThead>
{filteredData.map((item) => ( <TableTr>
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '25%' }}>
<Box w={100}> <Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd > <TableTd style={{ width: '25%' }}>
<Box w={150}> <Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd>{item.kategori?.name || 'No Category'}</TableTd> <TableTd style={{ width: '25%' }}>
<TableTd> <Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
<Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}> </TableTd>
<IconDeviceImacCog size={25} /> <TableTd style={{ width: '25%', textAlign: 'center' }}>
</Button> <Tooltip label="Kelola Prestasi" withArrow>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
size="sm"
>
<IconDeviceImacCog size={20} />
</Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4} style={{ textAlign: 'center' }}>
</Stack> <Text c="dimmed" py="md">Tidak ada data prestasi</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> {totalPages > 1 && (
<Pagination <Center mt="lg">
value={page} <Pagination
onChange={(newPage) => load(newPage)} value={page}
total={totalPages} onChange={load}
my={"md"} total={totalPages}
/> withEdges
</Center> size="sm"
/>
</Center>
)}
</Box> </Box>
) )
} }

View File

@@ -72,7 +72,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow> <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab <TabsTab
value={tab.value} value={tab.value}
leftSection={tab.icon} leftSection={tab.icon}

View File

@@ -87,7 +87,7 @@ function EditMediaSosial() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -51,6 +51,7 @@ function DetailMediaSosial() {
</Button> </Button>
<Paper <Paper
withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "60%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"

View File

@@ -69,7 +69,7 @@ export default function CreateMediaSosial() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -53,7 +53,7 @@ function ListMediaSosial({ search }: { search: string }) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Media Sosial</Title> <Title order={4}>Daftar Media Sosial</Title>
<Tooltip label="Tambah Media Sosial" withArrow> <Tooltip label="Tambah Media Sosial" withArrow>
@@ -66,34 +66,34 @@ function ListMediaSosial({ search }: { search: string }) {
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Media Sosial / Kontak</TableTh> <TableTh style={{ width: '25%' }}>Nama Media Sosial / Kontak</TableTh>
<TableTh>Gambar</TableTh> <TableTh style={{ width: '20%' }}>Gambar</TableTh>
<TableTh>Icon / No. Telepon</TableTh> <TableTh style={{ width: '20%' }}>Icon / No. Telepon</TableTh>
<TableTh>Aksi</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '25%', }}>
<Text fw={500}>{item.name}</Text> <Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '20%', }}>
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}> <Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden', }}>
{item.image?.link ? ( {item.image?.link ? (
<Image src={item.image.link} alt={item.name} fit="cover" /> <Image src={item.image.link} alt={item.name} fit="cover" />
) : ( ) : (
<Box bg={colors['blue-button']} w="100%" h="100%" /> <Box bg={colors['blue-button']} w="100%" h="100%" />
)} )}
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '20%', }}>
<Text truncate fz="sm" color="dimmed"> <Text truncate fz="sm" c="dimmed">
{item.iconUrl || item.noTelp || '-'} {item.iconUrl || item.noTelp || '-'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '15%'}}>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"

View File

@@ -145,7 +145,7 @@ function EditPejabatDesa() {
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -37,7 +37,7 @@ function Page() {
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Tooltip label="Edit Profil Pejabat" withArrow> <Tooltip label="Edit Profil Pejabat" withArrow>
<Button <Button
c="blue" c="green"
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
@@ -49,7 +49,7 @@ function Page() {
</GridCol> </GridCol>
</Grid> </Grid>
{dataArray.map((item) => ( {dataArray.map((item) => (
<Paper key={item.id} p="xl" bg={colors['BG-trans']} radius="md" shadow="xs"> <Paper key={item.id} p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: "sm", md: 100 }}> <Box px={{ base: "sm", md: 100 }}>
<Grid> <Grid>
<GridCol span={12}> <GridCol span={12}>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
@@ -99,7 +100,7 @@ function EditProgramInovasi() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
@@ -176,13 +177,16 @@ function EditProgramInovasi() {
required required
/> />
<TextInput <Box>
label="Deskripsi" <Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
placeholder="Masukkan deskripsi program inovasi" <EditEditor
value={formData.description} value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(htmlContent) => {
required setFormData((prev) => ({ ...prev, description: htmlContent }));
/> stateProgramInovasi.update.form.description = htmlContent;
}}
/>
</Box>
<TextInput <TextInput
label="Link Program Inovasi" label="Link Program Inovasi"

View File

@@ -51,6 +51,7 @@ function DetailProgramInovasi() {
p="lg" p="lg"
radius="md" radius="md"
shadow="sm" shadow="sm"
withBorder
> >
<Stack gap="md"> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
@@ -64,9 +65,23 @@ function DetailProgramInovasi() {
<Text fz="md" c="dimmed">{data.name || '-'}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Program"
radius="md"
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }}>{data.description || '-'}</Text> <Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
</Box> </Box>
<Box> <Box>
@@ -89,20 +104,6 @@ function DetailProgramInovasi() {
)} )}
</Box> </Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Program"
radius="md"
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Group gap="sm"> <Group gap="sm">
<Tooltip label="Hapus Program Inovasi" withArrow position="top"> <Tooltip label="Hapus Program Inovasi" withArrow position="top">
<Button <Button

View File

@@ -70,7 +70,7 @@ function CreateProgramInovasi() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -48,7 +48,7 @@ function ListProgramInovasi({ search }: { search: string }) {
return ( return (
<Box py={15}> <Box py={15}>
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm"> <Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
<Box mb="md" display="flex" <Box mb="md" display="flex"
style={{ justifyContent: 'space-between', alignItems: 'center' }} style={{ justifyContent: 'space-between', alignItems: 'center' }}
> >
@@ -91,9 +91,7 @@ function ListProgramInovasi({ search }: { search: string }) {
<Text fw={500}>{item.name}</Text> <Text fw={500}>{item.name}</Text>
</TableTd> </TableTd>
<TableTd style={{ maxWidth: 250 }}> <TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lineClamp={2}> <Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
{item.description}
</Text>
</TableTd> </TableTd>
<TableTd style={{ maxWidth: 250 }}> <TableTd style={{ maxWidth: 250 }}>
<Tooltip label="Buka tautan program" position="top" withArrow> <Tooltip label="Buka tautan program" position="top" withArrow>

View File

@@ -12,10 +12,12 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
Image
} from "@mantine/core"; } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from "@tabler/icons-react"; import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -97,82 +99,111 @@ function EditKolaborasiInovasi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit SDGs Desa</Title> Edit SDGs Desa
</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">
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar SDGs Desa
</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/*': [] }}
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>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
/>
</Box>
)}
</Box>
<TextInput <TextInput
label="Nama SDGs Desa"
placeholder="Masukkan nama SDGs Desa"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} required
placeholder="masukkan nama"
/> />
<TextInput <TextInput
label="Jumlah"
placeholder="Masukkan jumlah"
value={formData.jumlah} value={formData.jumlah}
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })} onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>} required
placeholder="masukkan jumlah" type="number"
/> />
<Box>
<Text fz={"md"} fw={"bold"}>File Image</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={{
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
}}
>
<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>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <Group justify="right">
<Text size="xl" inline> <Button
Drag file ke sini atau klik untuk pilih file onClick={handleSubmit}
</Text> leftSection={<IconDeviceFloppy size={20} />}
<Text size="sm" c="dimmed" inline mt={7}> loading={sdgsState.edit.loading}
Maksimal 5MB dan harus format image radius="md"
</Text> size="md"
</div> style={{
</Group> background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
</Dropzone> color: '#fff',
<Box> boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> }}
{previewImage ? ( >
<iframe Simpan
src={previewImage} </Button>
width="100%" </Group>
height="250px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -12,11 +11,11 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import sdgsDesa from '../../../_state/landing-page/sdgs-desa'; import sdgsDesa from '../../../_state/landing-page/sdgs-desa';
function DetailSDGSDesa() { function DetailSDGSDesa() {
const sdgsState = useProxy(sdgsDesa) const sdgsState = useProxy(sdgsDesa);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
sdgsState.findUnique.load(params?.id as string) sdgsState.findUnique.load(params?.id as string)
@@ -35,73 +34,104 @@ function DetailSDGSDesa() {
if (!sdgsState.findUnique.data) { if (!sdgsState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = sdgsState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail SDGS Desa</Text> </Button>
{sdgsState.findUnique.data ? (
<Paper key={sdgsState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: '100%', md: '60%' }}
<Text fw={"bold"} fz={"lg"}>Nama SDGS Desa</Text> bg={colors['white-1']}
<Text fz={"lg"}>{sdgsState.findUnique.data?.name}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fw={"bold"} fz={"lg"}>Jumlah</Text> >
<Text fz={"lg"}>{sdgsState.findUnique.data?.jumlah}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail SDGs Desa
<Text fw={"bold"} fz={"lg"}>Gambar</Text> </Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={sdgsState.findUnique.data?.image?.link} alt="gambar" />
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Flex gap={"xs"} mt={10}> <Stack gap="md">
<Box>
<Text fz="lg" fw="bold" mb={4}>Nama SDGs Desa</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold" mb={4}>Jumlah</Text>
<Text fz="md" c="dimmed">{data.jumlah || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold" mb={4}>Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar SDGs Desa'}
w={200}
h={200}
radius="md"
fit="contain"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Group gap="sm" mt="md">
<Tooltip label="Hapus SDGs Desa" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (sdgsState.findUnique.data) { setSelectedId(data.id);
setSelectedId(sdgsState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={sdgsState.delete.loading || !sdgsState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={sdgsState.delete.loading}
> >
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit SDGs Desa" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (sdgsState.findUnique.data) { onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${data.id}/edit`)}
router.push(`/admin/landing-page/SDGs-Desa/${sdgsState.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!sdgsState.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus SDGS Desa ini?' text="Apakah Anda yakin ingin menghapus SDGs Desa ini?"
/> />
</Box> </Box>
); );

View File

@@ -2,9 +2,9 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -56,88 +56,138 @@ function CreateSDGsDesa() {
router.push("/admin/landing-page/SDGs-Desa") router.push("/admin/landing-page/SDGs-Desa")
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah SDGs Desa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create SDGs Desa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Image</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar SDGs Desa
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid.')}
accept={{ maxSize={5 * 1024 ** 2}
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'], accept={{
}} 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
> }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> >
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
</Dropzone.Accept> <Dropzone.Accept>
<Dropzone.Reject> <IconUpload size={52} color={colors['blue-button']} stroke={1.5} />
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> </Dropzone.Accept>
</Dropzone.Reject> <Dropzone.Reject>
<Dropzone.Idle> <IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> </Dropzone.Reject>
</Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div style={{ textAlign: 'center' }}>
<Text size="xl" inline> <Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file Drag file ke sini atau klik untuk memilih
</Text>
<Text size="sm" c="dimmed" inline mt={7} display="block">
Maksimal 5MB (JPEG, JPG, PNG, GIF, WEBP, SVG)
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="md">
<Text fw={500} fz="sm" mb={4}>
Pratinjau Gambar
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Box
Maksimal 5MB dan harus format image style={{
</Text> border: '1px solid #e0e0e0',
</div> borderRadius: '8px',
</Group> overflow: 'hidden',
</Dropzone> maxWidth: '300px'
<Box> }}
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> >
{previewImage ? ( <Image
<iframe src={previewImage}
src={previewImage} alt="Preview"
width="100%" style={{ width: '100%', height: 'auto' }}
height="250px" />
style={{ border: "1px solid #ccc", borderRadius: "8px" }} </Box>
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box> </Box>
</Box> )}
</Box> </Box>
<TextInput <TextInput
label={
<Text fw="bold" fz="sm" mb={4}>
Nama SDGs Desa
</Text>
}
placeholder="Masukkan nama SDGs Desa"
value={stateSDGSDesa.create.form.name} value={stateSDGSDesa.create.form.name}
onChange={(val) => { onChange={(val) => {
stateSDGSDesa.create.form.name = val.target.value; stateSDGSDesa.create.form.name = val.target.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} required
placeholder='Masukkan judul' radius="md"
/> />
<TextInput
type='number' <TextInput
type="number"
label={
<Text fw="bold" fz="sm" mb={4}>
Jumlah
</Text>
}
placeholder="Masukkan jumlah"
value={stateSDGSDesa.create.form.jumlah} value={stateSDGSDesa.create.form.jumlah}
onChange={(val) => { onChange={(val) => {
stateSDGSDesa.create.form.jumlah = val.target.value; stateSDGSDesa.create.form.jumlah = val.target.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>} required
placeholder='Masukkan jumlah' min={0}
radius="md"
/> />
<Group>
<Button 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={stateSDGSDesa.create.loading}
color={colors['blue-button']}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; 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 { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import sdgsDesa from '../../_state/landing-page/sdgs-desa'; import sdgsDesa from '../../_state/landing-page/sdgs-desa';
@@ -17,7 +16,7 @@ function SdgsDesa() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='SDGs Desa' title='SDGs Desa'
placeholder='pencarian' placeholder='Cari SDGs Desa...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -57,20 +56,36 @@ function ListSdgsDesa({ search }: { search: string }) {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Desa Anti Korupsi' <Title order={4}>Daftar SDGs Desa</Title>
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create' <Tooltip label="Tambah SDGs Desa" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color={colors['blue-button']}
variant="light"
onClick={() => router.push('/admin/landing-page/SDGs-Desa/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama SDGs Desa</TableTh> <TableTh style={{ width: '60%' }}>Nama SDGs Desa</TableTh>
<TableTh>Jumlah SDGs Desa</TableTh> <TableTh style={{ width: '20%' }}>Jumlah</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody>
<TableTr>
<TableTd colSpan={3} style={{ textAlign: 'center', padding: '2rem' }}>
<Text c="dimmed">Tidak ada data SDGs Desa</Text>
</TableTd>
</TableTr>
</TableTbody>
</Table> </Table>
</Box> </Box>
</Paper> </Paper>
@@ -80,54 +95,70 @@ function ListSdgsDesa({ search }: { search: string }) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar SDGs Desa</Title>
title='SDGs Desa' <Tooltip label="Tambah SDGs Desa" withArrow>
href='/admin/landing-page/SDGs-Desa/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: 'auto' }}> color={colors['blue-button']}
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() => router.push('/admin/landing-page/SDGs-Desa/create')}
<TableTr> >
<TableTh>Nama SDGs Desa</TableTh> Tambah Baru
<TableTh>Jumlah SDGs Desa</TableTh> </Button>
<TableTh>Detail</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead> <Box style={{ overflowX: 'auto' }}>
<TableTbody> <Table highlightOnHover>
{filteredData.map((item) => ( <TableThead>
<TableTr key={item.id}> <TableTr>
<TableTd> <TableTh style={{ width: '60%' }}>Nama SDGs Desa</TableTh>
<Box w={100}> <TableTh style={{ width: '20%' }}>Jumlah</TableTh>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
</Box> </TableTr>
</TableTd> </TableThead>
<TableTd> <TableTbody>
<Text truncate="end" fz={"sm"}>{item.jumlah}</Text> {filteredData.map((item) => (
</TableTd> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '60%' }}>
<Button onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}> <Text fw={500} truncate="end" lineClamp={1}>
<IconDeviceImacCog size={25} /> {item.name}
</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed">
{item.jumlah || '0'}
</Text>
</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}
>
<IconDeviceImacCog size={18} />
</Button> </Button>
</TableTd> </Tooltip>
</TableTr> </TableTd>
))} </TableTr>
</TableTbody> ))}
</Table> </TableTbody>
</Box> </Table>
</Stack> </Box>
</Paper> </Paper>
<Center> <Center mt="lg">
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo(0, 0);
}} }}
total={totalPages} total={Math.max(1, totalPages)}
mt="md" withEdges
mb="md" radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -98,7 +98,7 @@ function EditKeteranganBankSampahTerdekat() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -54,7 +54,7 @@ function CreateKeteranganBankSampahTerdekat() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>

View File

@@ -78,7 +78,7 @@ function EditProgramKreatifDesa() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}

View File

@@ -129,7 +129,7 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
<Tooltip label="Edit" withArrow> <Tooltip label="Edit" withArrow>
<Button <Button
variant="light" variant="light"
color="blue" color="green"
size="sm" size="sm"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)} onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}
> >

View File

@@ -70,7 +70,7 @@ function EditProgramKreatifDesa() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}

View File

@@ -105,7 +105,7 @@ function ListKeunggulanProgram({ search }: { search: string }) {
<Tooltip label="Edit" withArrow> <Tooltip label="Edit" withArrow>
<Button <Button
variant="light" variant="light"
color="blue" color="green"
size="sm" size="sm"
onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/keunggulan-program/${item.id}`)} onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/keunggulan-program/${item.id}`)}
> >

View File

@@ -1,9 +1,9 @@
'use client' 'use client';
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik'; import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import colors from '@/con/colors'; 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 { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -73,28 +73,44 @@ function EditDaftarInformasiPublik() {
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Daftar Informasi Publik Desa Darmasaba</Title> 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 <TextInput
label="Jenis Informasi"
placeholder="Masukkan jenis informasi"
value={formData.jenisInformasi} value={formData.jenisInformasi}
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => { onChange={(val) => {
setFormData({ setFormData({
...formData, ...formData,
jenisInformasi: val.target.value jenisInformasi: val.target.value
}) });
}} }}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -103,19 +119,35 @@ function EditDaftarInformasiPublik() {
}} }}
/> />
</Box> </Box>
<TextInput <TextInput
type='date' type="date"
label="Tanggal Publikasi"
placeholder="Pilih tanggal publikasi"
value={formatDateForInput(formData.tanggal)} value={formatDateForInput(formData.tanggal)}
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
placeholder="masukkan tanggal publikasi"
onChange={(val) => { onChange={(val) => {
setFormData({ setFormData({
...formData, ...formData,
tanggal: val.target.value 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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

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

View File

@@ -1,70 +1,124 @@
'use client' 'use client'
import colors from '@/con/colors'; 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 { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import daftarInformasiPublik from '../../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik'; import daftarInformasiPublik from '../../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
export default function CreateBerita() { export default function CreateDaftarInformasi() {
const daftarInformasi = useProxy(daftarInformasiPublik) const daftarInformasi = useProxy(daftarInformasiPublik);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
// Reset state di valtio
daftarInformasi.create.form = { daftarInformasi.create.form = {
jenisInformasi: "", jenisInformasi: "",
deskripsi: "", deskripsi: "",
tanggal: "", tanggal: "",
}; };
// Reset state lokal
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
// Submit data berita if (!daftarInformasi.create.form.jenisInformasi) {
await daftarInformasi.create.create(); 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 try {
resetForm(); await daftarInformasi.create.create();
router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba") 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 ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Create Daftar Informasi Publik Desa Darmasaba</Title> Tambah Informasi Publik
<TextInput </Title>
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>} </Group>
placeholder="masukkan jenis informasi"
onChange={(val) => { <Paper
daftarInformasi.create.form.jenisInformasi = val.target.value w={{ base: '100%', md: '50%' }}
}} bg={colors['white-1']}
/> p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xl">
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
<CreateEditor Jenis Informasi
value={daftarInformasi.create.form.deskripsi} </Text>
onChange={(htmlContent) => { <TextInput
daftarInformasi.create.form.deskripsi = htmlContent; placeholder="Contoh: Profil Desa, Laporan Keuangan, dll"
value={daftarInformasi.create.form.jenisInformasi}
onChange={(e) => {
daftarInformasi.create.form.jenisInformasi = e.target.value;
}} }}
required
/> />
</Box> </Box>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>} <Box>
type="date" <Text fw="bold" fz="sm" mb={6}>
placeholder="Contoh: 2022-01-01" Deskripsi
value={daftarInformasi.create.form.tanggal} </Text>
onChange={(e) => (daftarInformasi.create.form.tanggal = e.currentTarget.value)} <Box style={{ border: '1px solid #dee2e6', borderRadius: '0.25rem' }}>
/> <CreateEditor
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> 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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,65 +1,108 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "Pegawai", label: "Pegawai",
value: "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", label: "Posisi Organisasi",
value: "posisiorganisasi", 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", label: "Struktur Organisasi",
value: "strukturorganisasi", 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 handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
} }
setActiveTab(value) setActiveTab(value);
} };
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Struktur PPID Desa Darmasaba</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Struktur PPID
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> 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> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <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> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -13,7 +13,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -134,142 +135,190 @@ export default function EditPegawaiPPID() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </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'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Data Pegawai PPID</Title> bg={colors['white-1']}
<TextInput p="lg"
label="Nama Lengkap" radius="md"
placeholder="Masukkan nama lengkap" shadow="sm"
value={formData.namaLengkap} style={{ border: '1px solid #e0e0e0' }}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })} >
/> <Stack gap="md">
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box > Nama Lengkap
<Dropzone </Text>
onDrop={(files) => { <TextInput
const file = files[0]; // Hanya ambil file pertama placeholder="Masukkan nama lengkap"
if (file) { value={formData.namaLengkap}
setFile(file); onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
setPreviewImage(URL.createObjectURL(file)); // Buat preview required
} />
}} </Box>
maxSize={5 * 1024 ** 2} // 5MB <Box>
accept={{ <Text fw="bold" fz="sm" mb={6}>
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] Gelar Akademik
}} </Text>
> <TextInput
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> placeholder="Contoh: S.Kom"
<Dropzone.Accept> value={formData.gelarAkademik}
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
</Dropzone.Accept> />
<Dropzone.Reject> </Box>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Box>
</Dropzone.Reject> <Text fw="bold" fz="sm" mb={6}>
<Dropzone.Idle> Foto Profil
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> </Text>
</Dropzone.Idle> <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> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
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 && (
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
width={280} radius="md"
height={180} style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
fit="cover"
radius="sm"
mt="md"
/> />
)} </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> </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 justify="flex-end" mt="md">
<Group>
<Button <Button
onClick={handleSubmit} 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> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box > </Box>
); );
} }

View File

@@ -2,41 +2,43 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, 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 { 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 { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailPegawai() { function DetailPegawai() {
const statePegawai = useProxy(stateStrukturPPID.pegawai) const statePegawai = useProxy(stateStrukturPPID.pegawai);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
statePegawai.findUnique.load(params?.id as string) statePegawai.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
statePegawai.delete.byId(selectedId) statePegawai.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ppid/struktur-ppid/pegawai") router.push("/admin/ppid/struktur-ppid/pegawai");
} }
} };
if (!statePegawai.findUnique.data) { if (!statePegawai.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = statePegawai.findUnique.data;
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb={10}>
@@ -44,91 +46,111 @@ function DetailPegawai() {
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
</Button> </Button>
</Box> </Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> <Paper
<Stack> withBorder
<Text fz={"xl"} fw={"bold"}>Detail Pegawai PPID</Text> w={{ base: "100%", md: "60%" }}
<Paper bg={colors['BG-trans']} p={'md'}> bg={colors['white-1']}
<Stack gap={"xs"}> 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> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Lengkap</Text> <Text fz="lg" fw="bold">Nama Lengkap</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.namaLengkap}</Text> <Text fz="md" c="dimmed">
</Box> {data.namaLengkap || '-'} {data.gelarAkademik || ''}
<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> </Text>
</Box> </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> <Box>
<Flex gap={"xs"}> <Text fz="lg" fw="bold">Posisi</Text>
<Button <Text fz="md" c="dimmed">{data.posisi?.nama || '-'}</Text>
onClick={() => { </Box>
if (statePegawai.findUnique.data) {
setSelectedId(statePegawai.findUnique.data.id); <Box>
setModalHapus(true); <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> </Button>
</Tooltip>
<Tooltip label="Edit Pegawai" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (statePegawai.findUnique.data) { onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${data.id}/edit`)}
router.push(`/admin/ppid/struktur-ppid/pegawai/${statePegawai.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!statePegawai.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -139,7 +161,7 @@ function DetailPegawai() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus pegawai ini?" text="Apakah Anda yakin ingin menghapus data pegawai ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,7 +3,7 @@
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; 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 { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -30,7 +30,7 @@ function CreatePegawaiPPID() {
telepon: "", telepon: "",
alamat: "", alamat: "",
posisiId: "", posisiId: "",
isActive: true, isActive: true,
}; };
}; };
@@ -53,14 +53,14 @@ function CreatePegawaiPPID() {
// Set status aktif secara otomatis // Set status aktif secara otomatis
stateOrganisasi.create.form.isActive = true; stateOrganisasi.create.form.isActive = true;
// Simpan ID gambar ke form // Simpan ID gambar ke form
stateOrganisasi.create.form.imageId = uploaded.id; stateOrganisasi.create.form.imageId = uploaded.id;
// Submit form // Submit form
await stateOrganisasi.create.submit(); await stateOrganisasi.create.submit();
// Reset form dan redirect // Reset form dan redirect
resetForm(); resetForm();
toast.success("Data pegawai berhasil ditambahkan"); toast.success("Data pegawai berhasil ditambahkan");
@@ -72,125 +72,201 @@ function CreatePegawaiPPID() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Create Pegawai</Title> Tambah Pegawai PPID
<TextInput </Title>
label="Nama Lengkap" </Group>
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>
<div> <Paper
<Text size="xl" inline> w={{ base: '100%', md: '50%' }}
Drag images here or click to select files bg={colors['white-1']}
</Text> p="lg"
<Text size="sm" c="dimmed" inline mt={7}> radius="md"
Attach as many files as you like, each file should not exceed 5mb shadow="sm"
</Text> style={{ border: '1px solid #e0e0e0' }}
</div> >
</Group> <Stack gap="md">
</Dropzone> <Box>
{previewImage && ( <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 <Image
src={previewImage.preview} src={previewImage.preview}
alt="Preview" alt="Preview"
width={280} width={200}
height={180} height={200}
fit="cover" fit="cover"
radius="sm" radius="md"
mt="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> </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 <Group justify="flex-end" mt="md">
onClick={handleSubmit} <Button
color="blue" onClick={handleSubmit}
> radius="md"
Simpan size="md"
</Button> 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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,97 @@
'use client'
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Badge, Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSparkles } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function Page() {
const params = useParams();
const id = params?.id as string;
const state = useProxy(profileLandingPageState.programInovasi);
const { data } = state.findUnique;
useShallowEffect(() => {
if (id) {
state.findUnique.load(id);
}
}, [id]);
if (!data) {
return (
<Box p="xl">
<Skeleton h={400} radius="lg" />
<Skeleton mt="md" h={30} radius="sm" />
<Skeleton mt="sm" h={20} w="80%" radius="sm" />
</Box>
);
}
return (
<Stack bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Paper
shadow="sm"
radius="xl"
p={{ base: 'md', md: 'xl' }}
mx="auto"
w="100%"
maw={1000}
bg="white"
style={{ border: '1px solid var(--mantine-color-gray-2)' }}
>
<Stack align="center" gap="md">
<Badge
leftSection={<IconSparkles size={16} />}
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
size="lg"
radius="lg"
>
Program Inovasi
</Badge>
<Title
order={1}
fz={{ base: '2rem', md: '2.8rem' }}
fw={800}
ta="center"
style={{ lineHeight: 1.3 }}
>
{data?.name}
</Title>
{data.image && (
<Image
src={data.image?.link}
alt={data.name}
fit="contain"
mah={400}
maw="80%"
radius="lg"
style={{
boxShadow: '0 0 25px rgba(0, 190, 255, 0.25)',
}}
/>
)}
<Text
fz={{ base: 'md', md: 'lg' }}
c="dimmed"
mt="sm"
lh={1.7}
dangerouslySetInnerHTML={{ __html: data?.description || '-' }}
/>
</Stack>
</Paper>
</Stack>
);
}
export default Page;

View File

@@ -15,7 +15,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
<motion.div whileHover={{ scale: 1.04 }}> <motion.div whileHover={{ scale: 1.04 }}>
<Tooltip label={`Lihat ${data.name}`} withArrow> <Tooltip label={`Lihat ${data.name}`} withArrow>
<Paper <Paper
onClick={() => router.push(`/${data.name}`)} onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="xl" p="xl"
radius="2xl" radius="2xl"
bg="white" bg="white"